113 lines
3.1 KiB
Swift
113 lines
3.1 KiB
Swift
|
|
import SwiftUI
|
||
|
|
import WebKit
|
||
|
|
|
||
|
|
struct EmbeddedWebAppView: View {
|
||
|
|
@Bindable var settings: SettingsStore
|
||
|
|
|
||
|
|
var body: some View {
|
||
|
|
PlatformWebView(script: settings.injectionScript, reloadToken: settings.webStateVersion)
|
||
|
|
.ignoresSafeArea()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#if os(macOS)
|
||
|
|
struct PlatformWebView: NSViewRepresentable {
|
||
|
|
let script: String
|
||
|
|
let reloadToken: UUID
|
||
|
|
|
||
|
|
func makeCoordinator() -> Coordinator {
|
||
|
|
Coordinator()
|
||
|
|
}
|
||
|
|
|
||
|
|
func makeNSView(context: Context) -> WKWebView {
|
||
|
|
context.coordinator.makeWebView(script: script)
|
||
|
|
}
|
||
|
|
|
||
|
|
func updateNSView(_ webView: WKWebView, context: Context) {
|
||
|
|
context.coordinator.update(webView: webView, script: script, reloadToken: reloadToken)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
#else
|
||
|
|
struct PlatformWebView: UIViewRepresentable {
|
||
|
|
let script: String
|
||
|
|
let reloadToken: UUID
|
||
|
|
|
||
|
|
func makeCoordinator() -> Coordinator {
|
||
|
|
Coordinator()
|
||
|
|
}
|
||
|
|
|
||
|
|
func makeUIView(context: Context) -> WKWebView {
|
||
|
|
context.coordinator.makeWebView(script: script)
|
||
|
|
}
|
||
|
|
|
||
|
|
func updateUIView(_ webView: WKWebView, context: Context) {
|
||
|
|
context.coordinator.update(webView: webView, script: script, reloadToken: reloadToken)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
#endif
|
||
|
|
|
||
|
|
final class Coordinator: NSObject, WKNavigationDelegate {
|
||
|
|
private var lastReloadToken: UUID?
|
||
|
|
private var pendingRefreshTask: Task<Void, Never>?
|
||
|
|
|
||
|
|
func makeWebView(script: String) -> WKWebView {
|
||
|
|
let configuration = WKWebViewConfiguration()
|
||
|
|
let contentController = WKUserContentController()
|
||
|
|
contentController.addUserScript(
|
||
|
|
WKUserScript(source: script, injectionTime: .atDocumentStart, forMainFrameOnly: true)
|
||
|
|
)
|
||
|
|
configuration.userContentController = contentController
|
||
|
|
|
||
|
|
let webView = WKWebView(frame: .zero, configuration: configuration)
|
||
|
|
webView.navigationDelegate = self
|
||
|
|
#if os(macOS)
|
||
|
|
webView.setValue(false, forKey: "drawsBackground")
|
||
|
|
#endif
|
||
|
|
loadIndex(into: webView)
|
||
|
|
return webView
|
||
|
|
}
|
||
|
|
|
||
|
|
func update(webView: WKWebView, script: String, reloadToken: UUID) {
|
||
|
|
guard lastReloadToken != reloadToken else {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
lastReloadToken = reloadToken
|
||
|
|
pendingRefreshTask?.cancel()
|
||
|
|
pendingRefreshTask = Task { @MainActor [weak webView] in
|
||
|
|
await Task.yield()
|
||
|
|
|
||
|
|
guard !Task.isCancelled, let webView else {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
webView.evaluateJavaScript(script) { _, _ in
|
||
|
|
DispatchQueue.main.async {
|
||
|
|
webView.reload()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private func loadIndex(into webView: WKWebView) {
|
||
|
|
let indexURL =
|
||
|
|
Bundle.main.url(forResource: "index", withExtension: "html", subdirectory: "WebApp/browser")
|
||
|
|
?? Bundle.main.url(forResource: "index", withExtension: "html", subdirectory: "WebApp")
|
||
|
|
|
||
|
|
guard let indexURL else {
|
||
|
|
let fallbackHTML = """
|
||
|
|
<html>
|
||
|
|
<body style="font-family:-apple-system;padding:32px;background:#08111d;color:#eff3ff">
|
||
|
|
<h2>Embedded chat bundle not found</h2>
|
||
|
|
<p>The Apple app expects the Angular client to be built into the bundled <code>WebApp</code> folder.</p>
|
||
|
|
</body>
|
||
|
|
</html>
|
||
|
|
"""
|
||
|
|
webView.loadHTMLString(fallbackHTML, baseURL: nil)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
webView.loadFileURL(indexURL, allowingReadAccessTo: indexURL.deletingLastPathComponent())
|
||
|
|
}
|
||
|
|
}
|