Spec für die randlose Navigationsinsel auf dem iPad. Der Baum sitzt heute vollständig innerhalb der Safe Area — deshalb erreicht kein safeAreaInset weiter unten den Streifen über der Insel. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
92 lines
6.9 KiB
Markdown
92 lines
6.9 KiB
Markdown
# Randlose Navigationsinsel auf dem iPad
|
|
|
|
## Kontext
|
|
|
|
Auf dem iPad schwebt die Navigation als Insel — wahlweise als Seitenleiste links oder als Kapsel oben. Über der Insel liegt ein schwarzer Streifen, der **nie** von Inhalt belegt wird, auch beim Scrollen nicht. Der Aufmacher hört an der Unterkante der Insel auf, statt hinter ihr bis zum Bildschirmrand durchzulaufen.
|
|
|
|
Im Arbeitsverzeichnis lagen bereits zwei ungetestete Versuche in diese Richtung:
|
|
|
|
- [`FlixyNavigationBar.swift`](../../../Flixy/Views/Navigation/FlixyNavigationBar.swift) — `.fixedSize()` am Menütitel, gegen das abgeschnittene „Mediathe…"
|
|
- [`RootView.swift`](../../../Flixy/Views/Navigation/RootView.swift) — `.padding(.top,…)` wurde zu `.safeAreaInset(edge: .top,…)`
|
|
|
|
Der zweite Versuch geht in die richtige Richtung, **greift aber eine Ebene zu tief**. Darum geht es in dieser Spec.
|
|
|
|
## Ursache
|
|
|
|
In der Kette `WindowGroup → RootView → library → HStack → tabs` ignoriert nichts die Safe Area. Das einzige `.ignoresSafeArea()` der Datei sitzt auf dem Player-Overlay (`RootView.swift:114`).
|
|
|
|
Damit sitzt der **gesamte** Navigationsbaum unterhalb der Statusleiste. Ein `safeAreaInset` auf `tabs` fügt Platz nur *innerhalb* dieses bereits beschnittenen Bereichs hinzu — der Inhalt kann beim Scrollen bestenfalls bis zur Unterkante der Statusleiste wandern, nie bis zum Bildschirmrand. Genau der Streifen, der übrig bleibt, ist der schwarze Balken.
|
|
|
|
`.background(Color.flixyCanvas)` färbt den Streifen, weil `.background` mit einer Farbe sich in die Safe Area ausdehnt. Deshalb ist er schwarz statt durchsichtig — und deshalb sah es nach einer gezeichneten Fläche aus statt nach fehlendem Inhalt.
|
|
|
|
## Entscheidungen (mit dem Nutzer abgestimmt)
|
|
|
|
| Thema | Entscheidung |
|
|
|---|---|
|
|
| Layout | **Randlos.** Der Aufmacher füllt von Anfang an bis zum Bildschirmrand, die Insel schwebt darauf. Kein Balken zu keiner Zeit. |
|
|
| Leiste oben | Blendet beim Scrollen aus, kommt zurück, wenn man **ganz oben** ist — nicht beim ersten Hochwischen. |
|
|
| Seitenleiste | Blendet **nicht** aus. Sie steht seitlich und stört beim Scrollen nicht. |
|
|
| Detailseiten | Die doppelte Chrome-Zeile („Staffel 2" unter der Insel) bleibt **unangetastet** und wird bewertet, wenn der Balken weg ist. Das Ausblenden beim Scrollen gilt dort trotzdem — ein Verhalten, das auf vier von sechs Bildschirmen greift, ist kein Verhalten, sondern ein Fehler. |
|
|
| iPhone | Unangetastet. Alles hier gilt nur für `sizeClass == .regular`. |
|
|
|
|
Verworfen wurde die zurückhaltendere Variante, bei der der Balken im Ruhezustand bestehen bleibt und der Inhalt nur beim Scrollen hindurchwandert. Begründung: der Ruhezustand ist das, was man beim Öffnen der App sieht.
|
|
|
|
## Umsetzung
|
|
|
|
### 1. Der Baum reicht bis zum Bildschirmrand
|
|
|
|
`.ignoresSafeArea(.container, edges: .top)` auf den `HStack` in `RootView.library`. Was die Safe Area vorher geleistet hat, kommt danach kontrolliert zurück:
|
|
|
|
- **Seitenleiste und Insel** bekommen ihren Abstand von oben explizit: Statusleistenhöhe + Rand. Ohne das rutschen sie in die Uhrzeit.
|
|
- **`tabs`** behält den `safeAreaInset`, aber mit der vollen Höhe: Statusleiste + Leiste. Damit beginnen alle Bildschirme unter der Insel, und ihr Inhalt wandert beim Scrollen bis zum Bildschirmrand durch.
|
|
- Die **Statusleistenhöhe** liest ein `GeometryReader` um `library` herum — also außerhalb des `HStack` und damit, bevor die Safe Area ignoriert wird. Innerhalb wäre sie bereits 0.
|
|
|
|
Der `safeAreaInset` ist bewusst kein `padding`: ein Abstand verkleinert die Fläche und der Inhalt hört an seiner Kante auf; ein Sicherheitsrand verschiebt nur den Anfang und lässt den Inhalt darunter durchlaufen.
|
|
|
|
### 2. Der Aufmacher füllt von y=0
|
|
|
|
Das ist der Unterschied zwischen „durchlässig" und „randlos": der Aufmacher zieht sich mit negativem Padding über das Inset zurück und beginnt am echten Bildschirmrand. Sein Text sitzt unten und bleibt frei.
|
|
|
|
`HeroArtwork` wächst über ein Seitenverhältnis, nicht über eine feste Höhe — „nach oben wachsen" heißt hier also: etwas mehr beschneiden.
|
|
|
|
### 3. Ein Verlauf für die Statusleiste
|
|
|
|
Uhrzeit und Batterie liegen künftig auf dem Bild. `HeroArtwork` bringt keinen Verlauf nach oben mit; es kommt einer dazu. Ohne ihn steht die Uhrzeit auf einem hellen Bild unlesbar da.
|
|
|
|
### 4. Die Leiste geht beim Scrollen weg
|
|
|
|
Ein `@Observable` im Environment trägt einen Wahrheitswert: ganz oben oder nicht. `RootView` liest ihn und schiebt die Insel mit Versatz und Deckkraft weg.
|
|
|
|
Gemeldet wird über einen Modifier `.flixyRevealsChrome()` auf der **äußeren, senkrechten** ScrollView jedes Bildschirms. Das Muster steht bereits im Haus (`ProgramGuide.swift:198`):
|
|
|
|
```swift
|
|
.onScrollGeometryChange(for: CGFloat.self) { $0.contentOffset.y + $0.contentInsets.top }
|
|
action: { _, y in visibility.isRevealed = y <= 0 }
|
|
```
|
|
|
|
`MediaRow` bekommt den Modifier **nicht** — das ist die waagerechte Reihe innerhalb der Startseite und würde beim seitlichen Blättern falsch melden.
|
|
|
|
Betroffene Bildschirme: `HomeView`, `LibraryView`, `SearchView`, `LiveTVView` sowie die Detailseiten (`PlayableDetailView`, `SeriesDetailView`). Bildschirme, die statt einer `ScrollView` eine `List` oder `Form` tragen — `SettingsView`, `DownloadsView` —, bekommen denselben Modifier; `onScrollGeometryChange` gilt dort ebenso.
|
|
|
|
**Das Inset bleibt konstant**, egal ob die Insel zu sehen ist. Ließe man es mitschrumpfen, verschöbe sich der Inhalt bei jedem Ein- und Ausblenden — der Inhalt würde beim Scrollen nachrucken.
|
|
|
|
Beim Wechsel des Eintrags wird zurückgesetzt: eine neue Seite beginnt oben, also ist die Insel da.
|
|
|
|
### 5. „Mediathe…"
|
|
|
|
`.fixedSize()` liegt bereits im Arbeitsverzeichnis und wird beim Bauen verifiziert. Reicht es nicht, liegt die Ursache darin, dass `Menu` mit `.menuStyle(.button)` sein Label beschneidet — dann muss der Griff auf die Kapsel statt auf den Text.
|
|
|
|
## Zwei Punkte, die gemessen und nicht geraten werden
|
|
|
|
1. **Setzt der eingebettete `UITabBarController` seine Safe Area erneut?** `TabChromeSuppressor` hängt einen UIKit-Controller in den Baum, und eingebettete Controller setzen ihre Insets gern selbst. Falls ja, kommt dort `additionalSafeAreaInsets` dazu.
|
|
2. **Greift der Scroll-Melder zentral?** Wenn `onScrollGeometryChange` an einer Stelle in `mediaStack` für alle Bildschirme meldet, ist das einer Stelle statt vier vorzuziehen. Bei verschachtelten ScrollViews (`MediaRow` in `HomeView`) ist aber unklar, welche gemeldet wird — das entscheidet die Messung.
|
|
|
|
## Fertig ist es, wenn
|
|
|
|
- Auf dem iPad ist über der Insel **kein** schwarzer Streifen mehr zu sehen, weder beim Öffnen noch beim Scrollen.
|
|
- Der Aufmacher läuft hinter der Insel hindurch bis zum oberen Bildschirmrand und verschwindet dort.
|
|
- Uhrzeit und Batterie sind über jedem Aufmacher lesbar, auch über einem hellen.
|
|
- Beim Runterscrollen verschwindet die Leiste oben; ganz oben ist sie wieder da. Der Inhalt ruckt dabei nicht.
|
|
- In der Leiste steht „Mediatheken", nicht „Mediathe…".
|
|
- Auf dem iPhone hat sich nichts verändert.
|