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.
This commit is contained in:
@@ -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<Bool>) {
|
||||
init(isLoading: Binding<Bool>, 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user