import Foundation import OSLog import SwiftUI import WebKit #if canImport(UIKit) import UIKit #endif private let log = Logger(subsystem: "ev.mana.webshell", category: "web") /// `WKNavigationDelegate` + `WKUIDelegate` für ``WebShellView``. Hält /// den reactive ``WebNavState`` aktuell, lenkt externe Links in den /// System-Browser und treibt Pull-to-Refresh an. /// /// Lebt auf `MainActor` (Closures von WKWebView liefern auf Main). /// KVO-Observations werden bei `deinit` entfernt. @MainActor public final class WebShellCoordinator: NSObject, WKNavigationDelegate, WKUIDelegate { let navState: WebNavState let openURL: OpenURLAction let config: WebShellConfig var lastTarget: WebTarget? private var progressObservation: NSKeyValueObservation? private var loadingObservation: NSKeyValueObservation? private var canGoBackObservation: NSKeyValueObservation? private var urlObservation: NSKeyValueObservation? #if canImport(UIKit) private weak var refreshControl: UIRefreshControl? #endif init(navState: WebNavState, openURL: OpenURLAction, config: WebShellConfig) { self.navState = navState self.openURL = openURL self.config = config super.init() } deinit { progressObservation?.invalidate() loadingObservation?.invalidate() canGoBackObservation?.invalidate() urlObservation?.invalidate() } func observe(webView: WKWebView) { progressObservation = webView.observe(\.estimatedProgress, options: [.new]) { [weak self] _, change in guard let value = change.newValue else { return } Task { @MainActor in self?.navState.estimatedProgress = value } } loadingObservation = webView.observe(\.isLoading, options: [.new]) { [weak self] _, change in guard let value = change.newValue else { return } Task { @MainActor in self?.navState.isLoading = value } } canGoBackObservation = webView.observe(\.canGoBack, options: [.new]) { [weak self] _, change in guard let value = change.newValue else { return } Task { @MainActor in self?.navState.canGoBack = value } } urlObservation = webView.observe(\.url, options: [.new]) { [weak self] _, change in let value = change.newValue ?? nil Task { @MainActor in self?.navState.currentURL = value } } } func load(_ url: URL, into webView: WKWebView) { log.info("WebShell load: \(url.absoluteString, privacy: .public)") navState.lastError = nil let request = URLRequest(url: url) webView.load(request) } #if canImport(UIKit) func attachRefresh(_ control: UIRefreshControl, webView: WKWebView) { refreshControl = control control.addAction( UIAction { [weak self, weak webView] _ in webView?.reload() Task { @MainActor in self?.refreshControl?.endRefreshing() } }, for: .valueChanged ) } #endif // MARK: - WKNavigationDelegate public func webView( _: WKWebView, decidePolicyFor navigationAction: WKNavigationAction ) async -> WKNavigationActionPolicy { guard let url = navigationAction.request.url else { return .allow } if let host = url.host, config.isAllowed(host: host) { return .allow } if url.scheme == "http" || url.scheme == "https" { log.info("WebShell → openURL extern: \(url.absoluteString, privacy: .public)") openURL(url) return .cancel } return .cancel } public func webView(_: WKWebView, didFail _: WKNavigation, withError error: Error) { log.warning("didFail: \(String(describing: error), privacy: .public)") navState.lastError = (error as NSError).localizedDescription } public func webView(_: WKWebView, didFailProvisionalNavigation _: WKNavigation, withError error: Error) { log.warning("didFailProvisional: \(String(describing: error), privacy: .public)") navState.lastError = (error as NSError).localizedDescription } public func webView(_: WKWebView, didFinish _: WKNavigation) { navState.lastError = nil } // MARK: - WKUIDelegate public func webView( _ webView: WKWebView, createWebViewWith _: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures _: WKWindowFeatures ) -> WKWebView? { // `target=_blank`-Links: kein neues Fenster, im aktuellen WebView // laden bzw. extern öffnen. if let url = navigationAction.request.url { if let host = url.host, config.isAllowed(host: host) { webView.load(navigationAction.request) } else { openURL(url) } } return nil } }