CxWebApp/swift-app/Sources/CxWebAppMac/WebView.swift
CxAI Agent 055e350108
Some checks are pending
build-and-push / image (push) Waiting to run
feat: initial CxWebApp (macOS shell + swift-app wired to CxLLM-SDK)
2026-05-16 14:32:01 -05:00

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)
}
}
}
}
}