// // BrowserWebView.swift // Prisme // // Author: Simon-Pierre Boucher // import SwiftUI import WebKit /// The single `UIViewRepresentable` wrapping `WKWebView` (CLAUDE.md ยง9). /// One instance renders the active tab; use `.id(tab.id)` so switching tabs /// tears down and rebuilds the attachment, returning the view to the pool. struct BrowserWebView: UIViewRepresentable { let tab: Tab let pool: WebViewPool func makeCoordinator() -> Coordinator { Coordinator(tab: tab, pool: pool) } func makeUIView(context: Context) -> WKWebView { let webView = pool.checkout(for: tab.containerID) context.coordinator.attach(to: webView) if let state = tab.interactionState { // Restores history, scroll position and form state. webView.interactionState = state } else if let url = tab.initialURL { webView.load(URLRequest(url: url)) } return webView } func updateUIView(_ webView: WKWebView, context: Context) {} static func dismantleUIView(_ webView: WKWebView, coordinator: Coordinator) { coordinator.detach(from: webView) } // MARK: - Coordinator @MainActor final class Coordinator: NSObject, WKNavigationDelegate, WKUIDelegate { private let tab: Tab private let pool: WebViewPool private var observations: [NSKeyValueObservation] = [] init(tab: Tab, pool: WebViewPool) { self.tab = tab self.pool = pool } func attach(to webView: WKWebView) { webView.navigationDelegate = self webView.uiDelegate = self tab.page.webView = webView tab.page.sync(from: webView) installRefreshControl(on: webView) observe(webView) if let prismeWebView = webView as? PrismeWebView { let page = tab.page prismeWebView.onSaveExcerpt = { page.onExcerptRequested?() } } } func detach(from webView: WKWebView) { observations.removeAll() (webView as? PrismeWebView)?.onSaveExcerpt = nil tab.interactionState = webView.interactionState tab.page.webView = nil tab.page.resetTransientState() webView.navigationDelegate = nil webView.uiDelegate = nil webView.scrollView.refreshControl = nil pool.checkin(webView, containerID: tab.containerID) } // MARK: State observation private func observe(_ webView: WKWebView) { // WKWebView KVO fires on the main thread; assumeIsolated is safe. let page = tab.page observations = [ webView.observe(\.title) { wv, _ in MainActor.assumeIsolated { page.sync(from: wv) } }, webView.observe(\.url) { wv, _ in MainActor.assumeIsolated { page.sync(from: wv) } }, webView.observe(\.estimatedProgress) { wv, _ in MainActor.assumeIsolated { page.sync(from: wv) } }, webView.observe(\.isLoading) { wv, _ in MainActor.assumeIsolated { page.sync(from: wv) } }, webView.observe(\.canGoBack) { wv, _ in MainActor.assumeIsolated { page.sync(from: wv) } }, webView.observe(\.canGoForward) { wv, _ in MainActor.assumeIsolated { page.sync(from: wv) } }, ] } // MARK: Pull to refresh private func installRefreshControl(on webView: WKWebView) { let control = UIRefreshControl() control.addTarget(self, action: #selector(handleRefresh), for: .valueChanged) webView.scrollView.refreshControl = control } @objc private func handleRefresh() { tab.page.reload() } // MARK: WKNavigationDelegate func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { webView.scrollView.refreshControl?.endRefreshing() tab.page.sync(from: webView) if let url = webView.url { tab.page.onDidFinishLoad?(url) } } func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { webView.scrollView.refreshControl?.endRefreshing() } func webView( _ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error ) { webView.scrollView.refreshControl?.endRefreshing() } // MARK: WKUIDelegate func webView( _ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures ) -> WKWebView? { // target=_blank: load in place for now; real popup-to-tab // routing comes with tab groups. if navigationAction.targetFrame == nil { webView.load(navigationAction.request) } return nil } } }