import SwiftUI import WebKit /// Phase ζ-1: SwiftUI-Hülle um `WKWebView`. Eine `WebShellView`-Instanz /// gehört zu einem Tab und behält ihren Web-State (Scroll-Position, /// Browser-History) während der Tab aktiv bleibt. /// /// **Verhalten:** /// - Lädt `target` beim ersten Auftauchen. /// - Wechselt `target` während die View lebt → lädt neue URL. /// - Pull-to-Refresh über `UIRefreshControl` (iOS). /// - External-Links (anderer Host, `target=_blank`) öffnen im System- /// Browser via `openURL`-Environment, nicht im WebView. /// - Cookies werden über das default `WKWebsiteDataStore` geteilt, /// sodass `CookieBridge` einmal injizierte `mana.access`-Cookies /// sichtbar sind. struct WebShellView: View { let target: WebTarget @State private var navState = WebNavState() @Environment(\.openURL) private var openURL var body: some View { VStack(spacing: 0) { if navState.isLoading { ProgressView(value: navState.estimatedProgress) .progressViewStyle(.linear) .frame(height: 2) } #if canImport(UIKit) WebViewRepresentable( target: target, navState: navState, openURL: openURL ) .background(ZitareTheme.background) #elseif canImport(AppKit) MacWebViewRepresentable( target: target, navState: navState, openURL: openURL ) .background(ZitareTheme.background) #endif if let error = navState.lastError { errorBar(error) } } } private func errorBar(_ message: String) -> some View { HStack(spacing: 12) { Image(systemName: "exclamationmark.triangle.fill") .foregroundStyle(ZitareTheme.warning) Text(message) .font(.caption) .lineLimit(2) Spacer() } .padding(.horizontal, 12) .padding(.vertical, 8) .background(ZitareTheme.muted) } } /// URL + monoton wachsende `reloadToken`. Ein neuer Token zwingt den /// WebView, dieselbe URL nochmal zu laden — wird gebraucht wenn der /// User auf einen Universal-Link tappt, der zur aktuellen URL führt. struct WebTarget: Equatable { let url: URL let reloadToken: Int init(url: URL, reloadToken: Int = 0) { self.url = url self.reloadToken = reloadToken } } /// Reactive Navigation-State, geteilt zwischen SwiftUI und Coordinator. @Observable final class WebNavState { var isLoading: Bool = false var estimatedProgress: Double = 0 var lastError: String? var currentURL: URL? var canGoBack: Bool = false } #if canImport(UIKit) import UIKit private struct WebViewRepresentable: UIViewRepresentable { let target: WebTarget let navState: WebNavState let openURL: OpenURLAction func makeCoordinator() -> WebShellCoordinator { WebShellCoordinator(navState: navState, openURL: openURL) } func makeUIView(context: Context) -> WKWebView { let config = WKWebViewConfiguration() config.websiteDataStore = .default() config.applicationNameForUserAgent = "ZitareNative/0.1 (iOS)" config.userContentController.addUserScript(WebShellScripts.hideWebHeader) let webView = WKWebView(frame: .zero, configuration: config) webView.navigationDelegate = context.coordinator webView.uiDelegate = context.coordinator webView.allowsBackForwardNavigationGestures = true webView.scrollView.refreshControl = makeRefreshControl( webView: webView, coordinator: context.coordinator ) context.coordinator.observe(webView: webView) context.coordinator.load(target.url, into: webView) 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 func makeCoordinator() -> WebShellCoordinator { WebShellCoordinator(navState: navState, openURL: openURL) } func makeNSView(context: Context) -> WKWebView { let config = WKWebViewConfiguration() config.websiteDataStore = .default() config.applicationNameForUserAgent = "ZitareNative/0.1 (macOS)" config.userContentController.addUserScript(WebShellScripts.hideWebHeader) let webView = WKWebView(frame: .zero, configuration: config) webView.navigationDelegate = context.coordinator webView.uiDelegate = context.coordinator webView.allowsBackForwardNavigationGestures = true context.coordinator.observe(webView: webView) context.coordinator.load(target.url, into: webView) 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