Drei Gründe, warum der Mac anders aussah, und keiner davon war Geschmack. **Die Schrift.** macOS rendert dieselben Textstile deutlich kleiner als iOS: `largeTitle` misst dort 26 Punkt statt 34, Fließtext 13 statt 17. Eine Oberfläche, die auf dem iPad stimmt, wirkt damit zusammengeschrumpft. Auf dem Mac stehen jetzt feste Größen — die von iOS. Das kostet dort Dynamic Type; vertretbar, weil macOS keine systemweite Textgröße kennt und man ein Fenster stattdessen größer ziehen kann. **Der Aufmacher wächst mit.** Das war der zweite, größere Teil: das Bild ist so breit wie das Fenster, die Schrift darauf war es nicht. Bei 1280 Punkt Breite wirkt ein 34-Punkt-Titel verloren, bei 834 auf dem iPad genau richtig. Titel, Schriftzug, Angaben und Abstände richten sich jetzt nach der Breite des Aufmachers — mit dem iPad als Bezug und einem Deckel, damit ein Fenster über die ganze Wand nicht in Plakatschrift endet. Die Beschreibung bekommt außerdem eine Zeilenbreite: ein Satz über 1280 Punkt ist unlesbar, das Auge verliert die Zeile. **Die Seitenleiste.** Sie richtet sich nach ihrem Inhalt, und bei Wörtern wie „Start" fällt sie auf eine Breite zusammen, in der schon der Kontoname darunter nicht mehr Platz hatte — daher das „S…". Die Beschriftungen haben jetzt eine Mindestbreite. **Die Formulare.** Auf dem Mac nahm SwiftUI den Spalten-Stil: Beschriftungen rechts ausgerichtet, keine abgesetzten Abschnitte. Jetzt derselbe gruppierte Stil wie auf iOS. Dazu: Scrollbalken sind weg. Der graue Streifen unter dem Aufmacher kam daher, dass in den Systemeinstellungen „Scrollbalken immer einblenden" steht — dagegen reicht `.hidden` nicht, `.never` schon. Und das Fenster öffnet mit 1280 × 820 statt in Notizzettelgröße, ohne graue Titelleiste. 263 Tests grün, iOS baut unverändert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
100 lines
4.1 KiB
Swift
100 lines
4.1 KiB
Swift
import SwiftUI
|
|
import FlixyDesign
|
|
import MediaKit
|
|
|
|
/// Der Aufmacher oben auf der Startseite — mehrere Titel, die sich abwechseln.
|
|
///
|
|
/// Ein einzelnes Bild sagt jedes Mal dasselbe. Hier wandert der Aufmacher alle
|
|
/// paar Sekunden weiter und lässt sich auch von Hand durchblättern; die Punkte
|
|
/// darunter zeigen, wie viele es sind und wo man gerade ist.
|
|
///
|
|
/// Das Weiterwandern hört auf, sobald jemand die Fläche berührt. Wer hier die
|
|
/// Hand hat, fährt selbst — sonst rutscht der Aufmacher genau in der Sekunde
|
|
/// weiter, in der man nach „Abspielen" greift, und man startet den falschen
|
|
/// Film. Das ist mir beim Ausprobieren selbst passiert.
|
|
///
|
|
/// Geblättert wird mit einem einrastenden Streifen, nicht mit `TabView`: den
|
|
/// Seitenstil gibt es auf dem Mac nicht, und ein TabView im TabView hatte
|
|
/// nebenbei die Eigenschaft, beim Wechsel kurz zwei Seiten gleichzeitig zu
|
|
/// zeigen.
|
|
struct HeroCarousel: View {
|
|
let items: [MediaItem]
|
|
|
|
/// Wie lange ein Titel stehen bleibt.
|
|
///
|
|
/// Lang genug, um Titel, Beschreibung und Knöpfe zu lesen — bei drei
|
|
/// Sekunden ist der Satz zu Ende, bevor man ihn gelesen hat.
|
|
private static let dwell: Duration = .seconds(7)
|
|
|
|
@State private var shownID: String?
|
|
/// Ob noch von selbst weitergewandert wird.
|
|
@State private var isAdvancing = true
|
|
/// Bewegung, die von selbst losgeht, ist für manche Menschen unangenehm
|
|
/// bis unbrauchbar. Wer sie abgestellt hat, blättert von Hand.
|
|
@Environment(\.accessibilityReduceMotion) private var reduceMotion
|
|
@Environment(\.flixyAccent) private var accent
|
|
|
|
private var currentIndex: Int {
|
|
items.firstIndex { $0.id == shownID } ?? 0
|
|
}
|
|
|
|
var body: some View {
|
|
VStack(spacing: FlixySpacing.regular) {
|
|
ScrollView(.horizontal) {
|
|
LazyHStack(spacing: 0) {
|
|
ForEach(items) { item in
|
|
HeroSection(item: item)
|
|
.containerRelativeFrame(.horizontal)
|
|
.id(item.id)
|
|
}
|
|
}
|
|
.scrollTargetLayout()
|
|
}
|
|
.scrollTargetBehavior(.paging)
|
|
.scrollPosition(id: $shownID)
|
|
// `.never`, nicht `.hidden`: wer in den Systemeinstellungen
|
|
// „Scrollbalken immer einblenden" gewählt hat, bekam sonst einen
|
|
// grauen Streifen quer unter den Aufmacher gelegt.
|
|
.scrollIndicators(.never)
|
|
.aspectRatio(FlixyAspectRatio.hero, contentMode: .fit)
|
|
// Jede Berührung hält das Weiterwandern an — auch die, die nur ein
|
|
// Antippen wird. Gleichzeitig und ohne Mindestweg, damit weder dem
|
|
// Blättern noch den Knöpfen etwas weggenommen wird.
|
|
.simultaneousGesture(
|
|
DragGesture(minimumDistance: 0).onChanged { _ in isAdvancing = false }
|
|
)
|
|
|
|
if items.count > 1 { indicator }
|
|
}
|
|
.task(id: shownID) { await advance() }
|
|
.onChange(of: items.map(\.id), initial: true) { _, ids in
|
|
// Beim ersten Zeichnen und nach jedem Aktualisieren: steht die
|
|
// gezeigte Kennung nicht mehr in der Liste, beginnt es von vorn.
|
|
if shownID == nil || !ids.contains(shownID ?? "") { shownID = ids.first }
|
|
}
|
|
}
|
|
|
|
private var indicator: some View {
|
|
HStack(spacing: FlixySpacing.snug) {
|
|
ForEach(items.indices, id: \.self) { position in
|
|
Capsule()
|
|
.fill(position == currentIndex ? accent : Color.flixyTextMuted.opacity(0.4))
|
|
.frame(width: position == currentIndex ? 18 : 6, height: 6)
|
|
}
|
|
}
|
|
.animation(.snappy(duration: 0.25), value: currentIndex)
|
|
.accessibilityHidden(true)
|
|
}
|
|
|
|
private func advance() async {
|
|
guard items.count > 1, !reduceMotion, isAdvancing, shownID != nil else { return }
|
|
try? await Task.sleep(for: Self.dwell)
|
|
guard !Task.isCancelled, isAdvancing else { return }
|
|
|
|
let next = (currentIndex + 1) % items.count
|
|
withAnimation(.easeInOut(duration: 0.5)) {
|
|
shownID = items[next].id
|
|
}
|
|
}
|
|
}
|