From c76ddfb7e3aa709c1e57ca3729f74ce1c0e6fdbf Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 25 Jun 2026 12:57:12 +0000 Subject: [PATCH] iOS: fix comment composer auto-closing when keyboard opens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The comment input overlay was gated on GeometryReader width > height to detect landscape. When the keyboard appears, the available height shrinks enough that width exceeds height, so SwiftUI removed the TextField from the view hierarchy and focus (keyboard) was lost immediately. Track device orientation instead for landscape UI gating, and prevent the comments ScrollView from dismissing the keyboard on scroll. Co-authored-by: Jakub Doboš --- .../FloatNative/Views/VideoPlayerView.swift | 139 ++++++++++-------- 1 file changed, 76 insertions(+), 63 deletions(-) diff --git a/apps/ios/FloatNative/Views/VideoPlayerView.swift b/apps/ios/FloatNative/Views/VideoPlayerView.swift index 4b94915..a82334c 100644 --- a/apps/ios/FloatNative/Views/VideoPlayerView.swift +++ b/apps/ios/FloatNative/Views/VideoPlayerView.swift @@ -60,6 +60,12 @@ struct VideoPlayerView: View { // Quality selector state @State private var isChangingQuality = false + /// Tracks physical device orientation for UI that must stay visible while + /// the keyboard is open. GeometryReader width/height flips to "landscape" + /// when the keyboard shrinks the available height, which was removing the + /// comment composer and immediately dismissing focus. + @State private var isDeviceLandscape = false + // Computed properties for UI private var hasLiked: Bool { userInteraction == .like @@ -74,82 +80,74 @@ struct VideoPlayerView: View { } var body: some View { - GeometryReader { geometry in - let isLandscape = geometry.size.width > geometry.size.height - - ZStack { - // Background - Color.adaptiveBackground - .ignoresSafeArea() - - // Persistent Layout (Always use portrait layout structure) - // When rotating to landscape, we trigger native fullscreen instead of swapping views - portraitLayout - - // Error state - if let error = errorMessage { - VStack(spacing: 16) { - Image(systemName: "exclamationmark.triangle") - .font(.system(size: 50)) - .foregroundStyle(.red) - - Text(error) - .foregroundStyle(Color.adaptiveText) - .multilineTextAlignment(.center) - - Button("Retry") { - Task { - await loadVideo() - } - } - .buttonStyle(LiquidGlassButtonStyle(tint: .floatplaneBlue)) - } - .padding() - } + ZStack { + // Background + Color.adaptiveBackground + .ignoresSafeArea() + + // Persistent Layout (Always use portrait layout structure) + // When rotating to landscape, we trigger native fullscreen instead of swapping views + portraitLayout + + // Error state + if let error = errorMessage { + VStack(spacing: 16) { + Image(systemName: "exclamationmark.triangle") + .font(.system(size: 50)) + .foregroundStyle(.red) + + Text(error) + .foregroundStyle(Color.adaptiveText) + .multilineTextAlignment(.center) - // Stream offline overlay (for livestreams that have ended) - if isStreamOffline && isLivestream { - StreamOfflineOverlay { - dismiss() + Button("Retry") { + Task { + await loadVideo() + } } + .buttonStyle(LiquidGlassButtonStyle(tint: .floatplaneBlue)) } + .padding() + } - // Bottom comment input overlay (only in portrait, not for livestreams, and when comments loaded successfully) - if !isLandscape && !isLivestream && !commentsLoadError { - VStack { - Spacer() - bottomCommentInput - } + // Stream offline overlay (for livestreams that have ended) + if isStreamOffline && isLivestream { + StreamOfflineOverlay { + dismiss() } } - #if !os(tvOS) - .navigationBarTitleDisplayMode(.inline) - #endif - .navigationBarBackButtonHidden(false) // Always show back button in "portrait" view - #if !os(tvOS) - .statusBar(hidden: false) // Always show status bar in "portrait" view - #endif - .toolbar(isLandscape ? .hidden : .visible, for: .tabBar) - .globalMenu() - // We listen to device orientation change because GeometryReader doesn't update - // reliably when the native player is in fullscreen mode (covering this view). - #if !os(tvOS) - .onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in - let orientation = UIDevice.current.orientation - - if orientation.isLandscape { - AVPlayerManager.shared.enterFullScreen() - } else if orientation == .portrait { - // Only exit fullscreen on standard portrait, ignore upside down - AVPlayerManager.shared.exitFullScreen() + + // Bottom comment input overlay (only in portrait, not for livestreams, and when comments loaded successfully) + if !isDeviceLandscape && !isLivestream && !commentsLoadError { + VStack { + Spacer() + bottomCommentInput } } - #endif } + #if !os(tvOS) + .navigationBarTitleDisplayMode(.inline) + #endif + .navigationBarBackButtonHidden(false) // Always show back button in "portrait" view + #if !os(tvOS) + .statusBar(hidden: false) // Always show status bar in "portrait" view + #endif + .toolbar(isDeviceLandscape ? .hidden : .visible, for: .tabBar) + .globalMenu() + // Device orientation drives landscape UI — not GeometryReader size, which + // falsely reads as landscape when the keyboard shrinks available height. + #if !os(tvOS) + .onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in + handleDeviceOrientationChange(UIDevice.current.orientation) + } + #endif .onAppear { // Initialize like/dislike counts from post currentLikes = post.likes currentDislikes = post.dislikes + #if !os(tvOS) + handleDeviceOrientationChange(UIDevice.current.orientation) + #endif } .task { await loadVideo() @@ -348,8 +346,23 @@ struct VideoPlayerView: View { .padding() .padding(.bottom, 100) // Space for floating comment input } + .scrollDismissesKeyboard(.never) + } + } + + #if !os(tvOS) + /// Update landscape UI state and fullscreen player from a device orientation. + /// Ignores face-up/face-down/unknown so incidental motion doesn't flicker UI. + private func handleDeviceOrientationChange(_ orientation: UIDeviceOrientation) { + if orientation.isLandscape { + isDeviceLandscape = true + AVPlayerManager.shared.enterFullScreen() + } else if orientation == .portrait { + isDeviceLandscape = false + AVPlayerManager.shared.exitFullScreen() } } + #endif // MARK: - Load Video