SPB Git

spb/prisme Public MIT

Navigateur iOS intelligent — chaque page comprise localement avant d'être affichée. SwiftUI · WebKit · Foundation Models, 100% on-device.

Swift 96.7% JavaScript 3.3%
5.2 KB · 157 lines swift
Raw Blame History
1//2//  BrowserWebView.swift3//  Prisme4//5//  Author: Simon-Pierre Boucher <contact@spboucher.ai>6//78import SwiftUI9import WebKit1011/// The single `UIViewRepresentable` wrapping `WKWebView` (CLAUDE.md §9).12/// One instance renders the active tab; use `.id(tab.id)` so switching tabs13/// tears down and rebuilds the attachment, returning the view to the pool.14struct BrowserWebView: UIViewRepresentable {15    let tab: Tab16    let pool: WebViewPool1718    func makeCoordinator() -> Coordinator {19        Coordinator(tab: tab, pool: pool)20    }2122    func makeUIView(context: Context) -> WKWebView {23        let webView = pool.checkout(for: tab.containerID)24        context.coordinator.attach(to: webView)2526        if let state = tab.interactionState {27            // Restores history, scroll position and form state.28            webView.interactionState = state29        } else if let url = tab.initialURL {30            webView.load(URLRequest(url: url))31        }32        return webView33    }3435    func updateUIView(_ webView: WKWebView, context: Context) {}3637    static func dismantleUIView(_ webView: WKWebView, coordinator: Coordinator) {38        coordinator.detach(from: webView)39    }4041    // MARK: - Coordinator4243    @MainActor44    final class Coordinator: NSObject, WKNavigationDelegate, WKUIDelegate {45        private let tab: Tab46        private let pool: WebViewPool47        private var observations: [NSKeyValueObservation] = []4849        init(tab: Tab, pool: WebViewPool) {50            self.tab = tab51            self.pool = pool52        }5354        func attach(to webView: WKWebView) {55            webView.navigationDelegate = self56            webView.uiDelegate = self57            tab.page.webView = webView58            tab.page.sync(from: webView)59            installRefreshControl(on: webView)60            observe(webView)61            if let prismeWebView = webView as? PrismeWebView {62                let page = tab.page63                prismeWebView.onSaveExcerpt = { page.onExcerptRequested?() }64            }65        }6667        func detach(from webView: WKWebView) {68            observations.removeAll()69            (webView as? PrismeWebView)?.onSaveExcerpt = nil70            tab.interactionState = webView.interactionState71            tab.page.webView = nil72            tab.page.resetTransientState()73            webView.navigationDelegate = nil74            webView.uiDelegate = nil75            webView.scrollView.refreshControl = nil76            pool.checkin(webView, containerID: tab.containerID)77        }7879        // MARK: State observation8081        private func observe(_ webView: WKWebView) {82            // WKWebView KVO fires on the main thread; assumeIsolated is safe.83            let page = tab.page84            observations = [85                webView.observe(\.title) { wv, _ in86                    MainActor.assumeIsolated { page.sync(from: wv) }87                },88                webView.observe(\.url) { wv, _ in89                    MainActor.assumeIsolated { page.sync(from: wv) }90                },91                webView.observe(\.estimatedProgress) { wv, _ in92                    MainActor.assumeIsolated { page.sync(from: wv) }93                },94                webView.observe(\.isLoading) { wv, _ in95                    MainActor.assumeIsolated { page.sync(from: wv) }96                },97                webView.observe(\.canGoBack) { wv, _ in98                    MainActor.assumeIsolated { page.sync(from: wv) }99                },100                webView.observe(\.canGoForward) { wv, _ in101                    MainActor.assumeIsolated { page.sync(from: wv) }102                },103            ]104        }105106        // MARK: Pull to refresh107108        private func installRefreshControl(on webView: WKWebView) {109            let control = UIRefreshControl()110            control.addTarget(self, action: #selector(handleRefresh), for: .valueChanged)111            webView.scrollView.refreshControl = control112        }113114        @objc private func handleRefresh() {115            tab.page.reload()116        }117118        // MARK: WKNavigationDelegate119120        func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {121            webView.scrollView.refreshControl?.endRefreshing()122            tab.page.sync(from: webView)123            if let url = webView.url {124                tab.page.onDidFinishLoad?(url)125            }126        }127128        func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {129            webView.scrollView.refreshControl?.endRefreshing()130        }131132        func webView(133            _ webView: WKWebView,134            didFailProvisionalNavigation navigation: WKNavigation!,135            withError error: Error136        ) {137            webView.scrollView.refreshControl?.endRefreshing()138        }139140        // MARK: WKUIDelegate141142        func webView(143            _ webView: WKWebView,144            createWebViewWith configuration: WKWebViewConfiguration,145            for navigationAction: WKNavigationAction,146            windowFeatures: WKWindowFeatures147        ) -> WKWebView? {148            // target=_blank: load in place for now; real popup-to-tab149            // routing comes with tab groups.150            if navigationAction.targetFrame == nil {151                webView.load(navigationAction.request)152            }153            return nil154        }155    }156}157