Hinter der Insel läuft jetzt das Wasser durch

Der schwarze Streifen über der Navigation war kein gezeichneter Balken,
sondern fehlender Inhalt: der gesamte Baum saß innerhalb des
Sicherheitsrands. Deshalb erreichte kein noch so gut gesetzter Rand
weiter unten die Fläche darüber — Inhalt kam dort nie hin.

Jetzt reißt der HStack den Rand oben auf und gibt den Platz kontrolliert
zurück: Seitenleiste und Insel bekommen ihren Abstand zur Statusleiste
explizit, der Inhalt bekommt ihn als Sicherheitsrand. Der Aufmacher
nimmt ihn sich wieder — um genau diese Strecke höher und um genau diese
Strecke größer, sodass unten alles bleibt, wo es war. Dazu ein knapper
Verlauf oben, damit Uhrzeit und Batterie auch über einem hellen Bild
lesbar bleiben.

Und die Leiste oben fährt beim Scrollen weg. Nur sie: der
Sicherheitsrand darunter bleibt stehen, sonst ruckte die Seite bei jedem
Ein- und Ausblenden unter dem Finger weg.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-09-13 22:52:07 +02:00
co-authored by Claude Opus 5
parent 9b3c6e2030
commit 81e22c4f97
13 changed files with 211 additions and 8 deletions
+68
View File
@@ -0,0 +1,68 @@
import SwiftUI
import Observation
/// Was oben über dem Inhalt schwebt — und wie viel Platz es einnimmt.
///
/// Zwei Dinge, die zusammengehören und deshalb an einer Stelle liegen: die
/// Höhe, die Statusleiste und Insel zusammen belegen, und ob die Insel gerade
/// zu sehen ist.
///
/// Der Umweg über ein geteiltes Objekt ist nötig, weil beide Enden weit
/// auseinanderliegen. Die Höhe kennt nur `RootView` — sie hängt am Gerät und
/// daran, ob die Navigation an der Seite oder oben sitzt. Brauchen tut sie der
/// Aufmacher, der unter ihr durchgreifen soll, und der sitzt drei Ebenen
/// tiefer in einem eigenen Navigationsstapel. Umgekehrt weiß nur die
/// Bildlaufansicht des gerade sichtbaren Bildschirms, ob man oben steht —
/// entscheiden muss es `RootView`.
@MainActor
@Observable
final class TopChrome {
/// Wie hoch Statusleiste und Insel zusammen bauen.
///
/// Der Wert, um den der Inhalt nach unten beginnt. Wer ihn überspringen
/// und bis an den Bildschirmrand greifen will, zieht sich um genau diese
/// Strecke nach oben.
var inset: CGFloat = 0
/// Ob die Insel gerade zu sehen ist.
///
/// Steht man ganz oben, ist sie da. Beim Scrollen fährt sie weg — der
/// Inhalt ist dann das Wichtigere, und die Navigation kommt zurück, sobald
/// man wieder oben ankommt.
var isRevealed = true
}
extension View {
/// Meldet dieser Bildlaufansicht an, ob sie ganz oben steht.
///
/// Gehört an die **äußere, senkrechte** Bildlaufansicht eines Bildschirms.
/// Nicht an die waagerechten Reihen darin: `onScrollGeometryChange` meldet
/// die nächstgelegene, und eine Reihe, die man seitwärts schiebt, steht
/// immer bei null — die Insel käme beim Blättern zurück, obwohl die Seite
/// weit unten steht.
func flixyRevealsTopChrome() -> some View {
modifier(RevealsTopChrome())
}
}
private struct RevealsTopChrome: ViewModifier {
@Environment(TopChrome.self) private var chrome
func body(content: Content) -> some View {
content
// Der Sicherheitsrand gehört dazugerechnet: `contentOffset` allein
// beginnt bei einer Ansicht mit Rand oben im Negativen, und „ganz
// oben" wäre dann nie null.
.onScrollGeometryChange(for: CGFloat.self) { geometry in
geometry.contentOffset.y + geometry.contentInsets.top
} action: { _, offset in
// Eine Schwelle statt genau null: das Gummiband am oberen Ende
// liefert auch im Ruhezustand Werte, die um Bruchteile eines
// Punktes schwanken, und die Insel flackerte sonst.
chrome.isRevealed = offset <= 1
}
// Wer einen Bildschirm verlässt, während er weit unten steht, käme
// sonst auf dem nächsten ohne Navigation an.
.onDisappear { chrome.isRevealed = true }
}
}