From 6c199edc12081624344679c64cdc3e9481dc9bb4 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Thu, 1 Oct 2026 22:49:43 +0200 Subject: [PATCH] Read the player controls safe area from the hosting window The safe area came from the first window of the first connected scene, which on the iPhone Duo can be the exterior display's window with its 84pt vertical status bar inset. In Split View on the interior display that inset became ~100pt of side padding on the top and bottom bars and truncated the duration label. A HostWindowReader now captures the window hosting the controls; the foreground scene's key window is the fallback until it is known. --- Yattee/Views/Player/PlayerControlsView.swift | 52 ++++++++++++++++++-- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/Yattee/Views/Player/PlayerControlsView.swift b/Yattee/Views/Player/PlayerControlsView.swift index 172a87ba..dbe3bf1b 100644 --- a/Yattee/Views/Player/PlayerControlsView.swift +++ b/Yattee/Views/Player/PlayerControlsView.swift @@ -148,6 +148,11 @@ struct PlayerControlsView: View { @State private var playNextTapCount = 0 /// Track safe area to force re-render when it changes @State private var currentSafeArea: UIEdgeInsets = .zero + /// Window hosting these controls, captured by `HostWindowReader`. Safe + /// area must come from here: a global scene lookup can land on another + /// display's window (iPhone Duo exterior with its 84pt status bar inset + /// while the player sits on the interior in Split View). + @State private var hostWindow: UIWindow? @Environment(\.statusBarVisibilityController) private var statusBarController /// Active player controls layout from preset (passed from parent to avoid flashing during view recreation) var activeLayout: PlayerControlsLayout = .default @@ -266,6 +271,11 @@ struct PlayerControlsView: View { // Sync local controls visibility to playerState for other views to observe playerState.controlsVisible = newValue ?? (playerState.playbackState == .paused) } + .background(HostWindowReader { window in + guard window !== hostWindow else { return } + hostWindow = window + currentSafeArea = windowSafeArea + }) .onAppear { // Update safe area on appear to ensure correct layout currentSafeArea = windowSafeArea @@ -394,13 +404,17 @@ struct PlayerControlsView: View { UIDevice.current.userInterfaceIdiom == .pad } - /// Get safe area from window (works even when view ignores safe area) + /// Get safe area from the hosting window (works even when view ignores + /// safe area). Falls back to the active scene's key window until the + /// host window is known. private var windowSafeArea: UIEdgeInsets { - UIApplication.shared.connectedScenes + if let hostWindow { + return hostWindow.safeAreaInsets + } + return UIApplication.shared.connectedScenes .compactMap { $0 as? UIWindowScene } - .first? - .windows - .first? + .first(where: { $0.activationState == .foregroundActive })? + .keyWindow? .safeAreaInsets ?? .zero } @@ -1679,4 +1693,32 @@ private struct SideSliderBackgroundModifier: ViewModifier { } } +/// Reports the `UIWindow` hosting the view, on every window move. +private struct HostWindowReader: UIViewRepresentable { + let onWindow: (UIWindow?) -> Void + + func makeUIView(context: Context) -> WindowObservingView { + let view = WindowObservingView() + view.isUserInteractionEnabled = false + view.onWindow = onWindow + return view + } + + func updateUIView(_ uiView: WindowObservingView, context: Context) { + uiView.onWindow = onWindow + } + + final class WindowObservingView: UIView { + var onWindow: ((UIWindow?) -> Void)? + + override func didMoveToWindow() { + super.didMoveToWindow() + let window = self.window + DispatchQueue.main.async { [weak self] in + self?.onWindow?(window) + } + } + } +} + #endif