Mac: näher am iPad — Schrift, Seitenleiste, Formulare

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>
This commit is contained in:
Scarriffle
2026-09-13 15:59:22 +02:00
co-authored by Claude Opus 5
parent d54954b9c2
commit f8ada44a8c
21 changed files with 174 additions and 56 deletions
+17
View File
@@ -67,6 +67,12 @@ struct FlixyApp: App {
var body: some Scene {
WindowGroup {
RootView()
// Eine Untergrenze, unter der die Oberfläche nicht mehr
// aufgeht: schmaler als das hier klappt die Seitenleiste zu
// und die Reihen zeigen anderthalb Kacheln.
#if os(macOS)
.frame(minWidth: 900, minHeight: 620)
#endif
.environment(session)
.environment(playback)
.environment(playbackSettings)
@@ -102,6 +108,17 @@ struct FlixyApp: App {
}
}
.modelContainer(modelContainer)
#if os(macOS)
// Ohne diese Angabe öffnet macOS ein Fenster von der Größe eines
// Notizzettels — und darin fällt der Anmeldebildschirm in sich
// zusammen, weil seine Abstände dehnbar sind.
.defaultSize(width: 1280, height: 820)
// Das Fenster darf nicht kleiner werden, als der Inhalt verträgt.
.windowResizability(.contentMinSize)
// Keine graue Titelleiste über einem dunklen Bild: die Ampelknöpfe
// schweben über dem Inhalt, wie man es von Fernseh-Apps kennt.
.windowStyle(.hiddenTitleBar)
#endif
}
}
+1 -1
View File
@@ -35,7 +35,7 @@ struct ParentLinks: View {
}
.padding(.horizontal, FlixySpacing.screenMargin)
}
.scrollIndicators(.hidden)
.scrollIndicators(.never)
}
}
+3 -3
View File
@@ -52,7 +52,7 @@ struct PlayableDetailView: View {
}
.padding(.bottom, FlixySpacing.section)
}
.scrollIndicators(.hidden)
.scrollIndicators(.never)
}
private var currentMedia: MediaVersion? {
@@ -162,7 +162,7 @@ struct TagStrip: View {
}
.padding(.horizontal, FlixySpacing.screenMargin)
}
.scrollIndicators(.hidden)
.scrollIndicators(.never)
}
}
@@ -209,7 +209,7 @@ struct CastRow: View {
}
.padding(.horizontal, FlixySpacing.screenMargin)
}
.scrollIndicators(.hidden)
.scrollIndicators(.never)
}
}
}
+1 -1
View File
@@ -34,7 +34,7 @@ struct SeriesDetailView: View {
}
.padding(.bottom, FlixySpacing.section)
}
.scrollIndicators(.hidden)
.scrollIndicators(.never)
.task(id: item.id) { await loadChildren() }
}
+4 -1
View File
@@ -52,7 +52,10 @@ struct HeroCarousel: View {
}
.scrollTargetBehavior(.paging)
.scrollPosition(id: $shownID)
.scrollIndicators(.hidden)
// `.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
+1 -1
View File
@@ -65,7 +65,7 @@ struct HomeView: View {
}
.padding(.bottom, FlixySpacing.section)
}
.scrollIndicators(.hidden)
.scrollIndicators(.never)
}
/// Bringt die Reihen vom Server in die Form, die eingestellt ist.
+1 -1
View File
@@ -37,7 +37,7 @@ struct MediaRow: View {
.scrollTargetLayout()
}
.scrollTargetBehavior(.viewAligned)
.scrollIndicators(.hidden)
.scrollIndicators(.never)
}
}
@@ -70,6 +70,7 @@ struct LibraryFilterSheet: View {
// gehen können — sichtbar an Picker-Werten, die in der Standardfarbe
// leuchteten, während die Schalter daneben schon die gewählte trugen.
.tint(accent)
.flixyForm()
.scrollContentBackground(.hidden)
.background(Color.flixyCanvas)
.navigationTitle(Text("filter.title"))
+2 -2
View File
@@ -56,7 +56,7 @@ struct ProgramGuide: View {
lanes
}
}
.scrollIndicators(.hidden)
.scrollIndicators(.never)
}
.background(Color.flixyCanvas)
.sheet(item: $selection) { selection in
@@ -193,7 +193,7 @@ struct ProgramGuide: View {
}
.frame(width: layout.totalWidth, alignment: .topLeading)
}
.scrollIndicators(.hidden)
.scrollIndicators(.never)
.frame(height: CGFloat(channels.count) * rowHeight)
.onScrollGeometryChange(for: CGFloat.self) { geometry in
geometry.contentOffset.x
+4 -1
View File
@@ -94,8 +94,11 @@ struct AccountBar: View {
.lineLimit(1)
}
}
// Ohne Vorrang staucht SwiftUI den Text vor dem Zwischenraum:
// in einer schmalen Leiste stand vom Namen nur noch „S…" da.
.layoutPriority(1)
Spacer(minLength: FlixySpacing.snug)
Spacer(minLength: 0)
// Zeigt nach oben: dorthin geht die Karte auf.
Image(systemName: "chevron.up")
+40 -18
View File
@@ -187,11 +187,11 @@ struct RootView: View {
TabSection {
ForEach(session.libraries) { library in
Tab(
library.title,
systemImage: symbol(for: library.kind),
value: SidebarDestination.library(key: library.id, title: library.title)
) {
mediaStack { LibraryView(library: library) }
} label: {
tabLabel(library.title, symbol: symbol(for: library.kind))
}
.customizationID("flixy.tab.library.\(library.id)")
}
@@ -201,37 +201,33 @@ struct RootView: View {
.customizationID("flixy.tab.libraries")
if session.supportsLiveTV {
Tab(
"sidebar.liveTV",
systemImage: "dot.radiowaves.left.and.right",
value: SidebarDestination.liveTV
) {
Tab(value: SidebarDestination.liveTV) {
mediaStack { LiveTVView() }
} label: {
tabLabel("sidebar.liveTV", symbol: "dot.radiowaves.left.and.right")
}
.customizationID("flixy.tab.liveTV")
}
} else {
Tab(
"sidebar.library",
systemImage: "books.vertical",
value: SidebarDestination.libraryHub
) {
Tab(value: SidebarDestination.libraryHub) {
mediaStack { LibraryHubView() }
} label: {
tabLabel("sidebar.library", symbol: "books.vertical")
}
.customizationID("flixy.tab.libraryHub")
}
Tab(
"sidebar.downloads",
systemImage: "arrow.down.circle",
value: SidebarDestination.downloads
) {
Tab(value: SidebarDestination.downloads) {
mediaStack { DownloadsView() }
} label: {
tabLabel("sidebar.downloads", symbol: "arrow.down.circle")
}
.customizationID("flixy.tab.downloads")
Tab("sidebar.settings", systemImage: "gearshape", value: SidebarDestination.settings) {
Tab(value: SidebarDestination.settings) {
mediaStack { SettingsView() }
} label: {
tabLabel("sidebar.settings", symbol: "gearshape")
}
.customizationID("flixy.tab.settings")
@@ -274,6 +270,32 @@ struct RootView: View {
}
}
/// Die Beschriftung eines Eintrags.
///
/// Auf dem Mac mit einer Mindestbreite. Die Seitenleiste richtet sich nach
/// ihrem Inhalt, und bei kurzen Wörtern wie „Start" fällt sie auf eine
/// Breite zusammen, in der schon der Kontoname darunter nicht mehr Platz
/// hat — auf dem iPad ist sie dreimal so breit, und genau dieser
/// Unterschied macht die beiden Oberflächen ungleich.
private func tabLabel(_ title: LocalizedStringKey, symbol: String) -> some View {
let label = Label(title, systemImage: symbol)
#if os(macOS)
return label.frame(minWidth: 170, alignment: .leading)
#else
return label
#endif
}
/// Dieselbe Beschriftung, wenn der Titel erst vom Server kommt.
private func tabLabel(_ title: String, symbol: String) -> some View {
let label = Label(title, systemImage: symbol)
#if os(macOS)
return label.frame(minWidth: 170, alignment: .leading)
#else
return label
#endif
}
private func symbol(for kind: MediaLibrary.Kind) -> String {
switch kind {
case .movie: "film"
@@ -67,6 +67,7 @@ struct AppearanceView: View {
PreviewRow()
}
}
.flixyForm()
.scrollContentBackground(.hidden)
.background(Color.flixyCanvas)
.navigationTitle(Text("appearance.title"))
@@ -115,6 +115,7 @@ struct DiagnosticsView: View {
// gehen können — sichtbar an Picker-Werten, die in der Standardfarbe
// leuchteten, während die Schalter daneben schon die gewählte trugen.
.tint(accent)
.flixyForm()
.scrollContentBackground(.hidden)
.background(Color.flixyCanvas)
.navigationTitle(Text("diagnostics.title"))
@@ -73,6 +73,7 @@ struct DownloadSettingsView: View {
}
}
.tint(accent)
.flixyForm()
.scrollContentBackground(.hidden)
.background(Color.flixyCanvas)
.navigationTitle(Text("sidebar.downloads"))
+1
View File
@@ -59,6 +59,7 @@ struct LanguagesView: View {
}
}
.tint(accent)
.flixyForm()
.scrollContentBackground(.hidden)
.background(Color.flixyCanvas)
.navigationTitle(Text("languages.title"))
@@ -86,6 +86,7 @@ struct PlaybackSettingsView: View {
}
}
.tint(accent)
.flixyForm()
.scrollContentBackground(.hidden)
.background(Color.flixyCanvas)
.navigationTitle(Text("settings.playback"))
@@ -46,6 +46,7 @@ struct ServerSettingsView: View {
}
}
.tint(accent)
.flixyForm()
.scrollContentBackground(.hidden)
.background(Color.flixyCanvas)
.navigationTitle(Text("settings.server"))
+1
View File
@@ -80,6 +80,7 @@ struct SettingsView: View {
// gehen können — sichtbar an Picker-Werten, die in der Standardfarbe
// leuchteten, während die Schalter daneben schon die gewählte trugen.
.tint(accent)
.flixyForm()
.scrollContentBackground(.hidden)
.background(Color.flixyCanvas)
.navigationTitle(Text("sidebar.settings"))