Files
yattee/Yattee/Views/Player/HingeLayoutObserver.swift
Arkadiusz Fal 99d3d31031 Add iPhone Duo tabletop player layout and exterior display insets
Observe the foldable hinge with `onHingeChange` (iOS 27.1): while it is
partially open and the portrait panel is hidden, the video is fitted to
the upper half of the screen and every control (title bar, center
controls, side sliders, bottom bar) moves to the lower half. Pinch to
fill is disabled in that state. `YATTEE_FORCE_TABLETOP=1` in the launch
environment forces the layout on simulators without a hinge control.

The exterior display has a vertical status bar with a trailing safe
area inset, which squeezed the fullscreen portrait player into the
content width. The video and controls layers now span the full window
width (shifted under the leading inset, like the existing vertical
handling) and the controls pad their edges by the sheet's actual
asymmetric horizontal insets instead of the symmetric max.

Requires the iOS 27.1 SDK (Xcode 27.1).
2026-09-30 22:02:12 +02:00

49 lines
1.6 KiB
Swift

//
// HingeLayoutObserver.swift
// Yattee
//
// Observes the foldable device hinge (iPhone Duo) and reports whether the
// player should use the "tabletop" layout: hinge partially open, device
// resting like a laptop, video on the upper half and controls on the lower.
//
import SwiftUI
#if os(iOS)
struct HingeLayoutObserver: ViewModifier {
@Binding var isTabletop: Bool
/// Debug override: launch with `YATTEE_FORCE_TABLETOP=1` to force the
/// tabletop layout on simulators without a hinge control.
private static let forceTabletop = ProcessInfo.processInfo.environment["YATTEE_FORCE_TABLETOP"] == "1"
func body(content: Content) -> some View {
if Self.forceTabletop {
content.onAppear { isTabletop = true }
} else if #available(iOS 27.1, *) {
content.onHingeChange { _, context in
// `hinge` is nil on devices without a hinge and when the view
// leaves a hierarchy that provides hinge updates.
let tabletop = context.hinge?.status == .partiallyOpen
guard tabletop != isTabletop else { return }
withAnimation(.easeInOut(duration: 0.3)) {
isTabletop = tabletop
}
}
} else {
content
}
}
}
extension View {
/// Tracks the device hinge and writes whether the tabletop player layout
/// should be active. No-op before iOS 27.1 and on devices without a hinge.
func observeHingeLayout(isTabletop: Binding<Bool>) -> some View {
modifier(HingeLayoutObserver(isTabletop: isTabletop))
}
}
#endif