Files
Flaro/Flixy/Session/OrientationLock.swift
T
ScarriffleandClaude Opus 5 4576eac67c Das Bild füllt den Schirm, und die Schrift bleibt darin
Vier Dinge am iPhone-Player, die zusammengehören, weil sie alle davon
handeln, wo der Bildschirm aufhört.

**Einpassen oder füllen.** `videoGravity` stand fest auf `.resizeAspect`,
und zwar in einer Zeile, die nach jedem Neuaufbau erneut läuft — eine
Wahl hätte dort also jeden Qualitätswechsel nicht überlebt. Jetzt steht
sie im Modell, und die Zeile liest sie. Umgelegt wird mit zwei Fingern
oder über einen Knopf; stufenlos wäre falsch, weil alles zwischen den
zwei Zuständen ein Bild ist, das weder ganz zu sehen ist noch den Schirm
füllt. Auf dem Mac erscheint der Knopf nicht: dort steht der
Vollbildknopf darüber, und zwei Knöpfe, die beide „grösser" bedeuten,
sind eine Zumutung.

**Untertitel im Füllmodus.** Dort reicht das Bild über die untere
Bildschirmkante hinaus, und genau dort zeichnet AVFoundation die
Untertitel. Statt sie anzuschneiden, wird die Bildebene um den
Sicherheitsrand eingerückt — rund zwanzig Punkt Bild gegen eine Zeile,
die man lesen kann. Dafür bekommt die Bildfläche echtes Layout: eine
`autoresizingMask` hält Ränder verhältnismässig, und ein fester Einzug
von zwanzig Punkten wäre im Querformat plötzlich vierzig gewesen.

**Die Bedienleiste kannte ihre Ränder nicht mehr.** Der Player bekommt
`ignoresSafeArea()` von der Wurzel und sieht danach überall null —
Schieberegler und Zeitangaben sassen im Home-Indikator, der Zurück-Pfeil
hinter der Dynamic Island, wo ihn niemand drücken konnte. Gemessen wird
jetzt eine Ebene weiter aussen, wo die Ränder noch gelten, und über die
Umgebung nach innen gereicht. Dieselbe Bauart wie der Akzent. Die Zeile
steht ganz am Ende der Kette: stünde sie vor dem Overlay, umschlösse das
Overlay sie, und ausgerechnet der Player ginge leer aus.

**Gedreht wird einmal, gesperrt wird nicht.** Die Maske blieb auf
`.landscape` stehen — wer im Bett auf der Seite lag, konnte nicht mehr
ins Hochformat drehen. Ein Film beginnt quer, weil er quer ist; danach
gehört die Entscheidung dem Nutzer. Dazu ein Drehknopf für den Fall,
dass die Ausrichtungssperre des Systems an ist, und ein Fehlerbehandler
an `requestGeometryUpdate`: eine abgelehnte Drehung ist dann eine
Einstellung und keine Panne.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-29 17:05:44 +02:00

111 lines
4.2 KiB
Swift

#if os(iOS)
import SwiftUI
import UIKit
/// Bestimmt, in welche Richtungen sich die App drehen darf.
///
/// In reinem SwiftUI gibt es dafür keinen Weg: `requestGeometryUpdate` dreht
/// zwar, aber das System dreht sofort zurück, wenn der Bildschirm es nicht
/// auch erlaubt. Erlaubt wird es über
/// `application(_:supportedInterfaceOrientationsFor:)` — und das gibt es nur
/// am klassischen App-Delegaten. Diese Klasse ist die Brücke dazwischen.
@MainActor
final class OrientationLock {
static let shared = OrientationLock()
private(set) var mask: UIInterfaceOrientationMask = .portrait
private init() {
// Auf dem iPad bleibt alles erlaubt: dort ist das Querformat beim
// Blättern der Normalfall, nicht die Ausnahme.
if UIDevice.current.userInterfaceIdiom == .pad {
mask = .all
}
}
/// Dreht zum Start der Wiedergabe ins Querformat — und gibt danach frei.
///
/// **Gedreht wird einmal, gesperrt wird nicht.** Vorher blieb die Maske
/// auf `.landscape` stehen, und damit liess sich im Player nicht mehr ins
/// Hochformat drehen. Ein Film beginnt quer, weil er quer ist; wer danach
/// im Bett auf der Seite liegt, soll drehen dürfen.
func lockLandscape() {
guard UIDevice.current.userInterfaceIdiom == .phone else { return }
apply(.allButUpsideDown, rotatingTo: .landscape)
}
/// Gibt die Ausrichtung wieder frei und dreht zurück ins Hochformat.
func releaseLandscape() {
guard UIDevice.current.userInterfaceIdiom == .phone else { return }
apply(.portrait, rotatingTo: .portrait)
}
/// Dreht auf Knopfdruck, ohne die Freiheit danach zu nehmen.
func rotate(to target: UIInterfaceOrientationMask) {
guard UIDevice.current.userInterfaceIdiom == .phone else { return }
apply(.allButUpsideDown, rotatingTo: target)
}
private func apply(
_ newMask: UIInterfaceOrientationMask,
rotatingTo target: UIInterfaceOrientationMask
) {
mask = newMask
guard let scene = UIApplication.shared.connectedScenes
.compactMap({ $0 as? UIWindowScene })
.first(where: { $0.activationState == .foregroundActive })
else { return }
// Erst die neue Erlaubnis bekanntgeben, dann drehen. In der anderen
// Reihenfolge lehnt das System die Drehung ab, weil die alte Maske
// noch gilt — und das sah aus wie „dreht sich nicht richtig".
scene.keyWindow?.rootViewController?.setNeedsUpdateOfSupportedInterfaceOrientations()
// **Mit Fehlerbehandler.** Bei eingeschalteter Ausrichtungssperre
// lehnt das System die Drehung ab. Ohne diesen Block wirft UIKit die
// Ablehnung in die Konsole und es sieht aus, als hinge die App —
// dabei ist es eine Einstellung des Nutzers und in Ordnung. Der Film
// läuft dann eben hochkant weiter.
scene.requestGeometryUpdate(.iOS(interfaceOrientations: target)) { fehler in
#if DEBUG
print("Drehung abgelehnt (vermutlich Ausrichtungssperre): \(fehler)")
#endif
}
}
}
/// Dreht beim Erscheinen ins Querformat und beim Verschwinden zurück.
struct LandscapeWhileVisible: ViewModifier {
/// Ob überhaupt gesperrt wird.
///
/// Für die allermeisten Filme ist das Querformat richtig — und für den,
/// der im Bett auf der Seite liegt, das Gegenteil davon.
let isEnabled: Bool
func body(content: Content) -> some View {
content
.onAppear { if isEnabled { OrientationLock.shared.lockLandscape() } }
.onDisappear { OrientationLock.shared.releaseLandscape() }
}
}
extension View {
/// Für den Vollbildplayer: Film ist Querformat.
func landscapeWhileVisible(_ isEnabled: Bool = true) -> some View {
modifier(LandscapeWhileVisible(isEnabled: isEnabled))
}
}
#else
import SwiftUI
/// Auf dem Mac dreht sich nichts.
///
/// Ein Fenster hat keine Ausrichtung, die sich sperren ließe — der Nutzer
/// bestimmt seine Größe. Die Stelle bleibt trotzdem bestehen, damit der
/// Player auf beiden Plattformen dieselbe Zeile schreiben kann.
extension View {
func landscapeWhileVisible(_ isEnabled: Bool = true) -> some View { self }
}
#endif