From bfe2cd20012d69d203bf7de4425ca5326fd0117a Mon Sep 17 00:00:00 2001 From: Rocky <72559939+hkgood@users.noreply.github.com> Date: Sat, 8 Aug 2026 10:53:46 +0800 Subject: [PATCH] fix(app): remove release-notes WebView bottom safe-area gap Extend the sheet WebView under the home indicator, keep scroll insets cleared after navigation, and neutralize remote page safe-area bottom padding in-app. --- .../Views/Components/RemoteWebView.swift | 48 +++++++++++++++---- OSGKeyboard/Views/ReleaseNotesSheet.swift | 15 +++++- 2 files changed, 54 insertions(+), 9 deletions(-) diff --git a/OSGKeyboard/Views/Components/RemoteWebView.swift b/OSGKeyboard/Views/Components/RemoteWebView.swift index 0ce0493..32fc108 100644 --- a/OSGKeyboard/Views/Components/RemoteWebView.swift +++ b/OSGKeyboard/Views/Components/RemoteWebView.swift @@ -1,7 +1,7 @@ // RemoteWebView.swift // OSGKeyboard ยท Main App // -// In-app WKWebView for remote HTTPS pages (e.g. GitHub Issues). +// In-app WKWebView for remote HTTPS pages (e.g. GitHub Issues, release notes). import SwiftUI import WebKit @@ -9,9 +9,12 @@ import WebKit struct RemoteWebView: UIViewRepresentable { let url: URL @Binding var isLoading: Bool + /// When true, strip page `env(safe-area-inset-*)` bottom padding after load + /// (sheets already lay out above the home indicator). + var neutralizeSafeAreaPadding: Bool = false func makeCoordinator() -> Coordinator { - Coordinator(isLoading: $isLoading) + Coordinator(isLoading: $isLoading, neutralizeSafeAreaPadding: neutralizeSafeAreaPadding) } func makeUIView(context: Context) -> WKWebView { @@ -19,11 +22,7 @@ struct RemoteWebView: UIViewRepresentable { webView.isOpaque = false webView.backgroundColor = .clear webView.scrollView.backgroundColor = .clear - // Sheet / NavigationStack already lays out inside the safe area. - // Automatic adjustment would add a second bottom inset and leave a dead band. - webView.scrollView.contentInsetAdjustmentBehavior = .never - webView.scrollView.contentInset = .zero - webView.scrollView.scrollIndicatorInsets = .zero + Self.applyScrollInsets(webView) webView.navigationDelegate = context.coordinator context.coordinator.loadedURL = url webView.load(URLRequest(url: url)) @@ -31,33 +30,66 @@ struct RemoteWebView: UIViewRepresentable { } func updateUIView(_ uiView: WKWebView, context: Context) { + context.coordinator.neutralizeSafeAreaPadding = neutralizeSafeAreaPadding + Self.applyScrollInsets(uiView) guard context.coordinator.loadedURL != url else { return } context.coordinator.loadedURL = url uiView.load(URLRequest(url: url)) } + /// Kill automatic safe-area insets that leave a dead band under the page. + static func applyScrollInsets(_ webView: WKWebView) { + let scroll = webView.scrollView + scroll.contentInsetAdjustmentBehavior = .never + scroll.contentInset = .zero + scroll.scrollIndicatorInsets = .zero + scroll.automaticallyAdjustsScrollIndicatorInsets = false + } + final class Coordinator: NSObject, WKNavigationDelegate { @Binding var isLoading: Bool var loadedURL: URL? + var neutralizeSafeAreaPadding: Bool - init(isLoading: Binding) { + init(isLoading: Binding, neutralizeSafeAreaPadding: Bool) { _isLoading = isLoading + self.neutralizeSafeAreaPadding = neutralizeSafeAreaPadding } func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { isLoading = true + RemoteWebView.applyScrollInsets(webView) } func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { isLoading = false + RemoteWebView.applyScrollInsets(webView) + guard neutralizeSafeAreaPadding else { return } + // Remote HTML may still use env(safe-area-inset-bottom); neutralize in-app. + webView.evaluateJavaScript( + """ + (function () { + var s = document.getElementById('osg-webview-safe-area-fix'); + if (!s) { + s = document.createElement('style'); + s.id = 'osg-webview-safe-area-fix'; + document.head.appendChild(s); + } + s.textContent = 'html{height:100%;} body{min-height:100%;padding-bottom:24px!important;}'; + })(); + """, + completionHandler: nil + ) } func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { isLoading = false + RemoteWebView.applyScrollInsets(webView) } func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { isLoading = false + RemoteWebView.applyScrollInsets(webView) } } } diff --git a/OSGKeyboard/Views/ReleaseNotesSheet.swift b/OSGKeyboard/Views/ReleaseNotesSheet.swift index 6670de5..46b1abe 100644 --- a/OSGKeyboard/Views/ReleaseNotesSheet.swift +++ b/OSGKeyboard/Views/ReleaseNotesSheet.swift @@ -36,7 +36,16 @@ struct ReleaseNotesSheet: View { colorScheme: resolvedColorScheme ) { ZStack { - RemoteWebView(url: url, isLoading: $isLoading) + // Fill the sheet body edge-to-edge (including home-indicator + // band). WKWebView otherwise sits above a blank safe-area strip. + RemoteWebView( + url: url, + isLoading: $isLoading, + neutralizeSafeAreaPadding: true + ) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .ignoresSafeArea(edges: .bottom) + if isLoading { ProgressView() .tint(palette.accent) @@ -52,6 +61,7 @@ struct ReleaseNotesSheet: View { ) } } + .frame(maxWidth: .infinity, maxHeight: .infinity) .background(palette.background.ignoresSafeArea()) .navigationTitle(AppL10n.string("releaseNotes.title", language: language)) .navigationBarTitleDisplayMode(.inline) @@ -63,6 +73,9 @@ struct ReleaseNotesSheet: View { } } } + // Force full-height sheet so the web view isn't clipped under a mid detent. + .presentationDetents([.large]) + .presentationDragIndicator(.visible) // Sheet can drop WindowGroup environment; re-assert language + appearance. .environment(\.locale, language.swiftUILocale) .preferredColorScheme(appearance.colorScheme)