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
|
// RemoteWebView.swift
|
||||||
// OSGKeyboard · Main App
|
// 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 SwiftUI
|
||||||
import WebKit
|
import WebKit
|
||||||
@@ -9,9 +9,12 @@ import WebKit
|
|||||||
struct RemoteWebView: UIViewRepresentable {
|
struct RemoteWebView: UIViewRepresentable {
|
||||||
let url: URL
|
let url: URL
|
||||||
@Binding var isLoading: Bool
|
@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 {
|
func makeCoordinator() -> Coordinator {
|
||||||
Coordinator(isLoading: $isLoading)
|
Coordinator(isLoading: $isLoading, neutralizeSafeAreaPadding: neutralizeSafeAreaPadding)
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeUIView(context: Context) -> WKWebView {
|
func makeUIView(context: Context) -> WKWebView {
|
||||||
@@ -19,11 +22,7 @@ struct RemoteWebView: UIViewRepresentable {
|
|||||||
webView.isOpaque = false
|
webView.isOpaque = false
|
||||||
webView.backgroundColor = .clear
|
webView.backgroundColor = .clear
|
||||||
webView.scrollView.backgroundColor = .clear
|
webView.scrollView.backgroundColor = .clear
|
||||||
// Sheet / NavigationStack already lays out inside the safe area.
|
Self.applyScrollInsets(webView)
|
||||||
// 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
|
|
||||||
webView.navigationDelegate = context.coordinator
|
webView.navigationDelegate = context.coordinator
|
||||||
context.coordinator.loadedURL = url
|
context.coordinator.loadedURL = url
|
||||||
webView.load(URLRequest(url: url))
|
webView.load(URLRequest(url: url))
|
||||||
@@ -31,33 +30,66 @@ struct RemoteWebView: UIViewRepresentable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func updateUIView(_ uiView: WKWebView, context: Context) {
|
func updateUIView(_ uiView: WKWebView, context: Context) {
|
||||||
|
context.coordinator.neutralizeSafeAreaPadding = neutralizeSafeAreaPadding
|
||||||
|
Self.applyScrollInsets(uiView)
|
||||||
guard context.coordinator.loadedURL != url else { return }
|
guard context.coordinator.loadedURL != url else { return }
|
||||||
context.coordinator.loadedURL = url
|
context.coordinator.loadedURL = url
|
||||||
uiView.load(URLRequest(url: 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 {
|
final class Coordinator: NSObject, WKNavigationDelegate {
|
||||||
@Binding var isLoading: Bool
|
@Binding var isLoading: Bool
|
||||||
var loadedURL: URL?
|
var loadedURL: URL?
|
||||||
|
var neutralizeSafeAreaPadding: Bool
|
||||||
|
|
||||||
init(isLoading: Binding<Bool>) {
|
init(isLoading: Binding<Bool>, neutralizeSafeAreaPadding: Bool) {
|
||||||
_isLoading = isLoading
|
_isLoading = isLoading
|
||||||
|
self.neutralizeSafeAreaPadding = neutralizeSafeAreaPadding
|
||||||
}
|
}
|
||||||
|
|
||||||
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
|
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
|
RemoteWebView.applyScrollInsets(webView)
|
||||||
}
|
}
|
||||||
|
|
||||||
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
||||||
isLoading = false
|
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) {
|
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
|
||||||
isLoading = false
|
isLoading = false
|
||||||
|
RemoteWebView.applyScrollInsets(webView)
|
||||||
}
|
}
|
||||||
|
|
||||||
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
|
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
|
||||||
isLoading = false
|
isLoading = false
|
||||||
|
RemoteWebView.applyScrollInsets(webView)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,16 @@ struct ReleaseNotesSheet: View {
|
|||||||
colorScheme: resolvedColorScheme
|
colorScheme: resolvedColorScheme
|
||||||
) {
|
) {
|
||||||
ZStack {
|
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 {
|
if isLoading {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
.tint(palette.accent)
|
.tint(palette.accent)
|
||||||
@@ -52,6 +61,7 @@ struct ReleaseNotesSheet: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
.background(palette.background.ignoresSafeArea())
|
.background(palette.background.ignoresSafeArea())
|
||||||
.navigationTitle(AppL10n.string("releaseNotes.title", language: language))
|
.navigationTitle(AppL10n.string("releaseNotes.title", language: language))
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.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.
|
// Sheet can drop WindowGroup environment; re-assert language + appearance.
|
||||||
.environment(\.locale, language.swiftUILocale)
|
.environment(\.locale, language.swiftUILocale)
|
||||||
.preferredColorScheme(appearance.colorScheme)
|
.preferredColorScheme(appearance.colorScheme)
|
||||||
|
|||||||
Reference in New Issue
Block a user