95 lines
3.6 KiB
Swift
95 lines
3.6 KiB
Swift
import SwiftUI
|
|
import WebKit
|
|
import Combine
|
|
|
|
/// SwiftUI wrapper around WKWebView. Exposes reload / navigation
|
|
/// commands through a Coordinator that holds the WKWebView reference.
|
|
struct WebView: NSViewRepresentable {
|
|
|
|
let url: URL
|
|
@Binding var canGoBack: Bool
|
|
@Binding var canGoForward: Bool
|
|
@Binding var isLoading: Bool
|
|
@Binding var pageTitle: String
|
|
let developerExtras: Bool
|
|
|
|
/// Token broadcast by `App` commands to trigger navigation actions.
|
|
let action: PassthroughSubject<WebAction, Never>
|
|
|
|
func makeCoordinator() -> Coordinator { Coordinator(self) }
|
|
|
|
func makeNSView(context: Context) -> WKWebView {
|
|
let cfg = WKWebViewConfiguration()
|
|
cfg.websiteDataStore = .nonPersistent()
|
|
if developerExtras {
|
|
cfg.preferences.setValue(true, forKey: "developerExtrasEnabled")
|
|
}
|
|
let wv = WKWebView(frame: .zero, configuration: cfg)
|
|
wv.navigationDelegate = context.coordinator
|
|
wv.uiDelegate = context.coordinator
|
|
wv.allowsBackForwardNavigationGestures = true
|
|
wv.allowsMagnification = true
|
|
wv.load(URLRequest(url: url))
|
|
|
|
context.coordinator.bind(webView: wv)
|
|
return wv
|
|
}
|
|
|
|
func updateNSView(_ wv: WKWebView, context: Context) {
|
|
// Reload if the URL prop changed (e.g. user edited it in Settings).
|
|
if wv.url?.absoluteString != url.absoluteString {
|
|
wv.load(URLRequest(url: url))
|
|
}
|
|
}
|
|
|
|
enum WebAction { case reload, back, forward, home, openInBrowser, copyURL }
|
|
|
|
// -------------------------------------------------------------------------
|
|
final class Coordinator: NSObject, WKNavigationDelegate, WKUIDelegate {
|
|
var parent: WebView
|
|
weak var webView: WKWebView?
|
|
private var cancellables = Set<AnyCancellable>()
|
|
private var observers: [NSKeyValueObservation] = []
|
|
|
|
init(_ p: WebView) { parent = p }
|
|
|
|
func bind(webView: WKWebView) {
|
|
self.webView = webView
|
|
observers.append(webView.observe(\.canGoBack) { [weak self] wv, _ in
|
|
Task { @MainActor in self?.parent.canGoBack = wv.canGoBack }
|
|
})
|
|
observers.append(webView.observe(\.canGoForward) { [weak self] wv, _ in
|
|
Task { @MainActor in self?.parent.canGoForward = wv.canGoForward }
|
|
})
|
|
observers.append(webView.observe(\.isLoading) { [weak self] wv, _ in
|
|
Task { @MainActor in self?.parent.isLoading = wv.isLoading }
|
|
})
|
|
observers.append(webView.observe(\.title) { [weak self] wv, _ in
|
|
Task { @MainActor in self?.parent.pageTitle = wv.title ?? "" }
|
|
})
|
|
|
|
parent.action
|
|
.receive(on: DispatchQueue.main)
|
|
.sink { [weak self] act in self?.handle(act) }
|
|
.store(in: &cancellables)
|
|
}
|
|
|
|
private func handle(_ a: WebAction) {
|
|
guard let wv = webView else { return }
|
|
switch a {
|
|
case .reload: wv.reload()
|
|
case .back: if wv.canGoBack { wv.goBack() }
|
|
case .forward: if wv.canGoForward { wv.goForward() }
|
|
case .home: wv.load(URLRequest(url: parent.url))
|
|
case .openInBrowser: if let u = wv.url { NSWorkspace.shared.open(u) }
|
|
case .copyURL:
|
|
if let u = wv.url?.absoluteString {
|
|
let pb = NSPasteboard.general
|
|
pb.clearContents()
|
|
pb.setString(u, forType: .string)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|