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>
111 lines
4.2 KiB
Swift
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
|