Die Leiste steht jetzt neben dem Inhalt, nicht darüber
Auf dem iPad beschneidet der TabView seinen Inhalt, auf dem Mac nicht. Gemessen: unter der Leiste war dort gar kein Bild — die Fläche blieb gleichmäßig dunkel, während der Aufmacher daneben bei 60 lag. Der negative Rand, mit dem der Aufmacher unter die Leiste greifen sollte, wurde schlicht abgeschnitten, und übrig blieb der schwarze Spalt dazwischen. Also weg damit. Die Leiste steht in einem HStack neben dem Inhalt: kein negativer Rand, keine Maske, kein Sicherheitsrand je Bildschirm, keine Sonderfälle für geschobene Bildschirme. Die Leiste hat ihren Abstand zu drei Seiten und zum Inhalt keinen — dort schließt er unmittelbar an. Die Startseite hat keine Navigationsleiste mehr. Sie war leer und belegte trotzdem ihre Höhe; der Aufmacher fing dadurch tiefer an als die Leiste daneben, und genau diese Kante sah man. Auf den geschobenen Bildschirmen bleibt sie, dort trägt sie den Zurück-Weg. Und Wischen auf dem Aufmacher schluckt das Scrollen nicht mehr. `minimumDistance: 0` ließ die Geste sofort greifen — wer nach oben wischte, bei dem passierte nichts. Jetzt vierundzwanzig Punkte und nur, wenn es waagerecht gemeint ist; das Anhalten des Wechsels übernimmt eine eigene Tippgeste. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
5b2d902cd9
commit
34593d9973
@@ -297,25 +297,34 @@ struct RootView: View {
|
||||
Group {
|
||||
#if os(iOS)
|
||||
if sizeClass == .regular {
|
||||
tabs
|
||||
// Der Platz für die Leiste: als **echter** Rand am ganzen
|
||||
// TabView, nicht als Sicherheitsrand je Bildschirm.
|
||||
//
|
||||
// Ein Sicherheitsrand wandert nicht in einen geschobenen
|
||||
// Bildschirm mit. Die Einstellungen schieben ihre
|
||||
// Unterseiten über gewöhnliche Verweise, und dort fehlte
|
||||
// er: die Beschriftungen lagen hinter der Leiste und waren
|
||||
// von der Maske abgeschnitten. Ein Rand am TabView gilt
|
||||
// für alles, was darin aufgeht.
|
||||
.padding(chromeInsets)
|
||||
.background { TabChromeSuppressor().frame(width: 0, height: 0) }
|
||||
.mask(alignment: .topLeading) { chromeMask }
|
||||
// Immer dieselbe Ausrichtung: wechselte sie mit der
|
||||
// Stellung, sprang die Leiste im Übergang an die neue Ecke,
|
||||
// statt dorthin zu wandern.
|
||||
.overlay(alignment: .topLeading) {
|
||||
chrome
|
||||
HStack(spacing: 0) {
|
||||
if placement == .sidebar {
|
||||
FlixySidebar(
|
||||
groups: navigationGroups,
|
||||
selection: $selection,
|
||||
placement: $placement.animation(Self.placementAnimation),
|
||||
layout: navigationLayout
|
||||
)
|
||||
.transition(.move(edge: .leading).combined(with: .opacity))
|
||||
}
|
||||
|
||||
tabs
|
||||
// Bündig mit der Oberkante der Leiste. Ohne das fing
|
||||
// der Aufmacher weiter oben an als sie, und genau an
|
||||
// dieser Kante sah man es.
|
||||
.padding(.top, placement == .sidebar ? FlixySidebar.margin : Self.topBarHeight)
|
||||
.background { TabChromeSuppressor().frame(width: 0, height: 0) }
|
||||
.overlay(alignment: .top) {
|
||||
if placement == .top {
|
||||
FlixyTopBar(
|
||||
groups: navigationGroups.map(visible),
|
||||
selection: $selection,
|
||||
placement: $placement.animation(Self.placementAnimation)
|
||||
)
|
||||
.transition(.move(edge: .top).combined(with: .opacity))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tabs
|
||||
}
|
||||
@@ -328,91 +337,9 @@ struct RootView: View {
|
||||
.onChange(of: placement) { _, new in storedPlacement = new }
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var chrome: some View {
|
||||
switch placement {
|
||||
case .sidebar:
|
||||
FlixySidebar(
|
||||
groups: navigationGroups,
|
||||
selection: $selection,
|
||||
placement: $placement.animation(Self.placementAnimation),
|
||||
layout: navigationLayout
|
||||
)
|
||||
// Sie kommt von der Seite herein und geht dorthin zurück. Ohne das
|
||||
// erschien sie schlagartig an ihrem Platz, und beim Umschalten war
|
||||
// nicht zu sehen, dass dasselbe umgezogen ist.
|
||||
.transition(.move(edge: .leading).combined(with: .opacity))
|
||||
/// Wie viel Platz die Leiste oben braucht.
|
||||
private static let topBarHeight: CGFloat = 52
|
||||
|
||||
case .top:
|
||||
// In der Mitte, und zwar erzwungen: die Leiste ist so breit wie
|
||||
// ihr Inhalt, und eine Überlagerung richtet sie dann nicht
|
||||
// zuverlässig mittig aus.
|
||||
HStack {
|
||||
Spacer(minLength: 0)
|
||||
FlixyTopBar(
|
||||
groups: navigationGroups.map(visible),
|
||||
selection: $selection,
|
||||
placement: $placement.animation(Self.placementAnimation)
|
||||
)
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
.transition(.move(edge: .top).combined(with: .opacity))
|
||||
}
|
||||
}
|
||||
|
||||
/// Wo der Inhalt überhaupt zu sehen sein darf.
|
||||
///
|
||||
/// Der Aufmacher greift nach links unter die Seitenleiste. Er scrollt
|
||||
/// dabei, die Leiste nicht — also blieb über und unter ihr je ein
|
||||
/// Streifen Bild stehen, den nichts abdeckte. Eine Maske kann das
|
||||
/// abschneiden, weil sie **hier** liegt, im festen Teil, und nicht im
|
||||
/// Inhalt, der wandert.
|
||||
///
|
||||
/// Sichtbar ist die Vereinigung aus zwei Flächen: alles ab der rechten
|
||||
/// Kante der Leiste, und dazu genau die Fläche der Leiste. Links davon —
|
||||
/// der schmale Streifen zum Fensterrand, ober- und unterhalb der Leiste —
|
||||
/// bleibt nichts übrig.
|
||||
@ViewBuilder
|
||||
private var chromeMask: some View {
|
||||
switch placement {
|
||||
case .sidebar:
|
||||
ZStack(alignment: .topLeading) {
|
||||
Color.black
|
||||
// Bis an die **Kante der Leiste**, nicht bis an die
|
||||
// Inhaltskante: dazwischen liegen zwölf Punkte, und die
|
||||
// standen als schwarze Lücke zwischen Bild und Leiste.
|
||||
// Jetzt reicht das Bild bis an sie heran.
|
||||
.padding(.leading, FlixySidebar.width + FlixySidebar.margin)
|
||||
RoundedRectangle(cornerRadius: FlixyRadius.panel, style: .continuous)
|
||||
.frame(width: FlixySidebar.width)
|
||||
.padding(FlixySidebar.margin)
|
||||
}
|
||||
case .top:
|
||||
// Oben greift nichts über den Rand hinaus; eine Maske hätte nichts
|
||||
// zu tun und könnte nur schaden.
|
||||
Color.black
|
||||
}
|
||||
}
|
||||
|
||||
private var chromeInsets: EdgeInsets {
|
||||
switch placement {
|
||||
case .sidebar:
|
||||
EdgeInsets(
|
||||
top: 0,
|
||||
leading: FlixySidebar.width + FlixySidebar.margin * 2,
|
||||
bottom: 0,
|
||||
trailing: 0
|
||||
)
|
||||
case .top:
|
||||
EdgeInsets(top: 52, leading: 0, bottom: 0, trailing: 0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Die Einträge, aus denen die Leiste gebaut wird.
|
||||
///
|
||||
/// Dieselbe Liste für beide Formen: was an der Seite steht, steht auch
|
||||
/// oben. Sonst wäre das Umschalten ein Wechsel der App und nicht nur
|
||||
/// einer der Anordnung.
|
||||
private var navigationGroups: [NavigationGroup] {
|
||||
var groups: [NavigationGroup] = [
|
||||
NavigationGroup(id: "main", title: nil, entries: [
|
||||
@@ -466,53 +393,18 @@ struct RootView: View {
|
||||
/// nicht; es passiert einfach nichts.
|
||||
private func mediaStack<Content: View>(@ViewBuilder content: () -> Content) -> some View {
|
||||
NavigationStack {
|
||||
chromed { content() }
|
||||
.navigationDestination(for: MediaRoute.self) { route in
|
||||
chromed { MediaDetailView(route: route) }
|
||||
}
|
||||
content()
|
||||
.navigationDestination(for: MediaRoute.self) { MediaDetailView(route: $0) }
|
||||
.navigationDestination(for: LibraryRoute.self) { route in
|
||||
chromed {
|
||||
if let library = session.libraries.first(where: { $0.id == route.id }) {
|
||||
LibraryView(library: library)
|
||||
}
|
||||
if let library = session.libraries.first(where: { $0.id == route.id }) {
|
||||
LibraryView(library: library)
|
||||
}
|
||||
}
|
||||
.navigationDestination(for: LiveTVRoute.self) { _ in
|
||||
chromed { LiveTVView() }
|
||||
}
|
||||
.navigationDestination(for: SearchRoute.self) { _ in
|
||||
chromed { SearchView() }
|
||||
}
|
||||
.navigationDestination(for: LiveTVRoute.self) { _ in LiveTVView() }
|
||||
.navigationDestination(for: SearchRoute.self) { _ in SearchView() }
|
||||
}
|
||||
}
|
||||
|
||||
/// Gibt einem Bildschirm den Platz, den unsere Leiste einnimmt.
|
||||
///
|
||||
/// Je Bildschirm und nicht einmal um den Stapel herum: weder der `TabView`
|
||||
/// noch der `NavigationStack` reichen einen Sicherheitsrand an ihren Inhalt
|
||||
/// weiter — der Aufmacher lag beide Male zur Hälfte hinter der Leiste.
|
||||
///
|
||||
/// Als **Sicherheitsrand**, nicht als Abstand. Der Unterschied ist der
|
||||
/// ganze Witz: eine Reihe bleibt damit fensterbreit und schiebt ihre
|
||||
/// Kacheln beim Wischen hinter die Leiste, stellt aber im Ruhezustand
|
||||
/// keine dorthin. Mit einem gewöhnlichen Abstand endete sie an der Kante.
|
||||
/// Sagt einem Bildschirm, wie weit er nach links ausbrechen darf.
|
||||
///
|
||||
/// Nur noch das: den Platz nimmt sich der Rand am TabView. Hier bleibt die
|
||||
/// Angabe für den Aufmacher, der bewusst darüber hinausgreift.
|
||||
@ViewBuilder
|
||||
private func chromed<Content: View>(@ViewBuilder content: () -> Content) -> some View {
|
||||
#if os(iOS)
|
||||
content()
|
||||
.environment(
|
||||
\.flixyChromeInset,
|
||||
sizeClass == .regular ? max(chromeInsets.leading - FlixySidebar.margin, 0) : 0
|
||||
)
|
||||
#else
|
||||
content()
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Die Breite der Seitenleiste auf dem Mac.
|
||||
///
|
||||
/// Sie richtet sich nach ihrem Inhalt. Ohne Untergrenze fällt sie bei
|
||||
|
||||
Reference in New Issue
Block a user