Die Ränder kommen jetzt von der Stelle, die sie kennt

Mein voriger Versuch war an der Quelle falsch, nicht an der Rechnung.

**Was ich gebaut hatte:** die Sicherheitsränder in `RootView` messen und
über die Umgebung an den Player reichen. Gemessen in einer Ansicht,
angewendet in einer anderen, über eine Drehung hinweg — drei
Gelegenheiten, daneben zu liegen. Im Simulator ging es gut, auf dem
Gerät nicht: dort standen im Querformat links 75 und rechts 28 Punkt
Rand, und der Untertitel wurde von der Bildschirmkante abgeschnitten.

**Warum SwiftUI hier nicht hilft.** Der Sicherheitsrand ist dort kein
Wert, den man erfragt, sondern einer, den man verbraucht: wer ihn
respektiert, sieht null, und wer ihn mit `ignoresSafeArea` überschreitet,
sieht danach ebenfalls null. Beide Auswege habe ich durchgespielt und
beide gemessen. Nimmt man der Ansicht das `ignoresSafeArea`, sitzen die
Knöpfe richtig — aber die Bildfläche verliert es mit, und das Bild ist im
Querformat um die Notch-Breite schmaler. Schiebt man einen
`GeometryReader` dazwischen, passiert dasselbe.

**Das Fenster weiß es immer**, in der richtigen Ausrichtung und in dem
Moment, in dem gefragt wird. `ScreenSafeArea` ist die eine Stelle in
dieser App, die so fragt, und die eine, an der es keinen SwiftUI-Weg
gibt. Nachgeholt wird der Wert bei jeder Änderung der Fläche — das ist
genau der Moment des Drehens.

Nachgemessen im Simulator gegen die Jellyfin-Attrappe: im Querformat
meldet das Fenster 0/62/62/20, und damit stehen Kopfzeile, Schieberegler
und Zeitangaben vollständig im sichtbaren Bereich, während das Bild bis
an die Kanten reicht.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-09-29 17:32:50 +02:00
co-authored by Claude Opus 5
parent 6326c8c24c
commit 22f824b000
4 changed files with 98 additions and 62 deletions
+9 -15
View File
@@ -73,11 +73,6 @@ struct RootView: View {
/// Bereichs, der den Sicherheitsrand gleich darauf ignoriert — innerhalb
/// wäre er längst null.
@State private var statusBarHeight: CGFloat = 0
/// Die vollen Sicherheitsränder des Bildschirms.
///
/// Hier gemessen und nicht im Player: der bekommt `ignoresSafeArea()` und
/// sieht danach überall null. Siehe `flixyScreenInsets`.
@State private var screenInsets = EdgeInsets()
var body: some View {
Group {
@@ -121,12 +116,9 @@ struct RootView: View {
// Hier und nicht weiter innen: ab dem `HStack` wird der obere
// Sicherheitsrand ignoriert, damit der Inhalt bis an den
// Bildschirmrand reicht. Wer danach misst, bekommt null.
.onGeometryChange(for: EdgeInsets.self) { proxy in
proxy.safeAreaInsets
} action: { neue in
screenInsets = neue
statusBarHeight = neue.top
}
.onGeometryChange(for: CGFloat.self) { proxy in
proxy.safeAreaInsets.top
} action: { statusBarHeight = $0 }
// Hier statt in der Scene: eine View wertet Beobachtungsänderungen
// zuverlässig neu aus, ein Scene-Body nicht.
.flixyAccent(theme)
@@ -152,6 +144,12 @@ struct RootView: View {
// ist, und der Übergang ist ruhig.
.overlay {
if playback.presentation == .full, let player {
// **Bildfüllend, und der Player passt drinnen selbst auf.**
//
// Ohne diese Zeile verliert die Bildfläche ihr
// `ignoresSafeArea` mit — das Bild wäre im Querformat um die
// Notch-Breite schmaler. Wo der Schirm aufhört, holt sich der
// Player deshalb aus dem Fenster; siehe `ScreenSafeArea`.
PlayerView(model: player)
.ignoresSafeArea()
.transition(.move(edge: .bottom))
@@ -167,10 +165,6 @@ struct RootView: View {
// bleiben, die es auf dem neuen Server gar nicht gibt.
if newPhase == .ready, case .library = selection { selection = .home }
}
// **Ganz am Ende der Kette.** Die Umgebung fliesst von aussen nach
// innen: stünde die Zeile vor `.overlay`, umschlösse das Overlay sie
// und der Player bekäme den Wert nicht. Hier umschliesst sie beides.
.flixyScreenInsets(screenInsets)
.onChange(of: sizeClass) { _, _ in
// Beim Drehen oder Teilen des Fensters wechselt die Form der
// Navigation — und mit ihr, welche Ziele es überhaupt gibt. Eine