import SwiftUI import WebKit /// SwiftUI-Hülle um `WKWebView`. Eine Instanz gehört üblicherweise zu /// einem Tab/Screen und behält ihren Web-State (Scroll-Position, /// Browser-History) während die View lebt. /// /// **Verhalten:** /// - Lädt `target` beim ersten Auftauchen. /// - Wechselt `target` während die View lebt → lädt neue URL (oder /// reloaded, wenn `target.reloadToken` sich erhöht). /// - Pull-to-Refresh über `UIRefreshControl` (iOS / iPadOS). /// - Links auf nicht-gelisteten Hosts (siehe ``WebShellConfig/allowedHosts``) /// und `target=_blank` öffnen im System-Browser via /// `OpenURLAction`, nicht im WebView. /// - Cookies werden über `WKWebsiteDataStore.default()` geteilt. /// /// **Theme-Hint:** /// `config.backgroundColor` ist der Hintergrund hinter dem WKWebView /// — verhindert weißen Flash bis zum first paint. Apps mit Dark-Theme /// setzen das auf ihren Theme-Background. public struct WebShellView: View { let target: WebTarget let config: WebShellConfig @State private var navState = WebNavState() @Environment(\.openURL) private var openURL public init(target: WebTarget, config: WebShellConfig) { self.target = target self.config = config } public var body: some View { VStack(spacing: 0) { if navState.isLoading { ProgressView(value: navState.estimatedProgress) .progressViewStyle(.linear) .tint(config.progressTint) .frame(height: 2) } #if canImport(UIKit) WebViewRepresentable( target: target, navState: navState, openURL: openURL, config: config ) .frame(maxWidth: .infinity, maxHeight: .infinity) .background(config.backgroundColor) #elseif canImport(AppKit) MacWebViewRepresentable( target: target, navState: navState, openURL: openURL, config: config ) .frame(maxWidth: .infinity, maxHeight: .infinity) .background(config.backgroundColor) #endif if let error = navState.lastError { errorBar(error) } } .frame(maxWidth: .infinity, maxHeight: .infinity) } private func errorBar(_ message: String) -> some View { HStack(spacing: 12) { Image(systemName: "exclamationmark.triangle.fill") .foregroundStyle(config.errorIconColor) Text(message) .font(.caption) .lineLimit(2) .foregroundStyle(config.errorForegroundColor) Spacer() } .padding(.horizontal, 12) .padding(.vertical, 8) .background(config.errorBackgroundColor) } } #if canImport(UIKit) import UIKit private struct WebViewRepresentable: UIViewRepresentable { let target: WebTarget let navState: WebNavState let openURL: OpenURLAction let config: WebShellConfig func makeCoordinator() -> WebShellCoordinator { WebShellCoordinator(navState: navState, openURL: openURL, config: config) } func makeUIView(context: Context) -> WKWebView { let wkConfig = WKWebViewConfiguration() wkConfig.websiteDataStore = .default() wkConfig.applicationNameForUserAgent = config.userAgent for script in config.userScripts { wkConfig.userContentController.addUserScript(script) } let webView = WKWebView(frame: .zero, configuration: wkConfig) webView.navigationDelegate = context.coordinator webView.uiDelegate = context.coordinator webView.allowsBackForwardNavigationGestures = true // Ohne diese drei flackert WKWebView bis zum first paint weiß // gegen das App-Theme — egal was der SwiftUI-Container als // Background setzt. webView.isOpaque = false webView.backgroundColor = .clear webView.scrollView.backgroundColor = .clear webView.scrollView.refreshControl = makeRefreshControl( webView: webView, coordinator: context.coordinator ) context.coordinator.observe(webView: webView) context.coordinator.load(target.url, into: webView) context.coordinator.lastTarget = target return webView } func updateUIView(_ webView: WKWebView, context: Context) { let coord = context.coordinator if coord.lastTarget != target { coord.load(target.url, into: webView) coord.lastTarget = target } } private func makeRefreshControl( webView: WKWebView, coordinator: WebShellCoordinator ) -> UIRefreshControl { let refresh = UIRefreshControl() coordinator.attachRefresh(refresh, webView: webView) return refresh } } #elseif canImport(AppKit) import AppKit private struct MacWebViewRepresentable: NSViewRepresentable { let target: WebTarget let navState: WebNavState let openURL: OpenURLAction let config: WebShellConfig func makeCoordinator() -> WebShellCoordinator { WebShellCoordinator(navState: navState, openURL: openURL, config: config) } func makeNSView(context: Context) -> WKWebView { let wkConfig = WKWebViewConfiguration() wkConfig.websiteDataStore = .default() wkConfig.applicationNameForUserAgent = config.userAgent for script in config.userScripts { wkConfig.userContentController.addUserScript(script) } let webView = WKWebView(frame: .zero, configuration: wkConfig) webView.navigationDelegate = context.coordinator webView.uiDelegate = context.coordinator webView.allowsBackForwardNavigationGestures = true // macOS-Pendant zu UIView.isOpaque=false — sonst weißer Flash // vor first paint. webView.setValue(false, forKey: "drawsBackground") context.coordinator.observe(webView: webView) context.coordinator.load(target.url, into: webView) context.coordinator.lastTarget = target return webView } func updateNSView(_ webView: WKWebView, context: Context) { let coord = context.coordinator if coord.lastTarget != target { coord.load(target.url, into: webView) coord.lastTarget = target } } } #endif