Created
December 1, 2023 20:22
-
-
Save davbeck/2edad5bc555ef2867528a89f1288d5a9 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Combine | |
import SwiftUI | |
import WebKit | |
@Observable final class WebViewContent: NSObject { | |
let id = UUID() | |
var url: URL? | |
var title: String? | |
var isLoading: Bool = false | |
var canGoBack: Bool = false | |
var canGoForward: Bool = false | |
fileprivate let _reload = PassthroughSubject<Void, Never>() | |
func reload() { | |
_reload.send() | |
} | |
fileprivate let _stopLoading = PassthroughSubject<Void, Never>() | |
func stopLoading() { | |
_stopLoading.send() | |
} | |
fileprivate let _goBack = PassthroughSubject<Void, Never>() | |
func goBack() { | |
_goBack.send() | |
} | |
fileprivate let _goForward = PassthroughSubject<Void, Never>() | |
func goForward() { | |
_goForward.send() | |
} | |
} | |
#if os(macOS) | |
typealias ViewRepresentable = NSViewRepresentable | |
#else | |
typealias ViewRepresentable = UIViewRepresentable | |
#endif | |
struct WebView: ViewRepresentable { | |
var content: WebViewContent | |
func makeCoordinator() -> Coordinator { | |
Coordinator() | |
} | |
#if os(macOS) | |
func makeNSView(context: Context) -> WKWebView { | |
WKWebView() | |
} | |
func updateNSView(_ webView: WKWebView, context: Context) { | |
updateView(webView, context: context) | |
} | |
#else | |
func makeUIView(context: Context) -> WKWebView { | |
WKWebView() | |
} | |
func updateUIView(_ webView: WKWebView, context: Context) { | |
updateView(webView, context: context) | |
} | |
#endif | |
private func updateView(_ webView: WKWebView, context: Context) { | |
context.coordinator.parent = self | |
context.coordinator.webView = webView | |
if context.coordinator.reportedURL != content.url { | |
context.coordinator.reportedURL = content.url | |
if let url = content.url { | |
webView.load(URLRequest(url: url)) | |
} else { | |
webView.load(URLRequest(url: URL(string: "about:blank")!)) | |
} | |
} | |
} | |
class Coordinator: NSObject { | |
private var contentObservers: Set<AnyCancellable> = [] | |
var parent: WebView? { | |
didSet { | |
guard parent?.content !== oldValue?.content else { return } | |
contentObservers = [] | |
guard let content = parent?.content else { return } | |
content._reload | |
.sink { [weak self] in | |
self?.webView?.reload() | |
} | |
.store(in: &contentObservers) | |
content._goBack | |
.sink { [weak self] in | |
self?.webView?.goBack() | |
} | |
.store(in: &contentObservers) | |
content._goForward | |
.sink { [weak self] in | |
self?.webView?.goForward() | |
} | |
.store(in: &contentObservers) | |
content._stopLoading | |
.sink { [weak self] in | |
self?.webView?.stopLoading() | |
} | |
.store(in: &contentObservers) | |
} | |
} | |
var reportedURL: URL? | |
private var webViewObservers: Set<AnyCancellable> = [] | |
var webView: WKWebView? { | |
didSet { | |
guard webView != oldValue else { return } | |
if oldValue?.navigationDelegate === self { | |
oldValue?.navigationDelegate = nil | |
} | |
if oldValue?.uiDelegate === self { | |
oldValue?.uiDelegate = nil | |
} | |
webViewObservers = [] | |
guard let webView else { return } | |
webView.customUserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15" | |
webView.isInspectable = true | |
webView.navigationDelegate = self | |
webView.uiDelegate = self | |
webView.publisher(for: \.url, options: .initial) | |
.receive(on: RunLoop.main) | |
.sink { [weak self] in | |
self?.reportedURL = $0 | |
guard self?.parent?.content.url != $0 else { return } | |
self?.parent?.content.url = $0 | |
} | |
.store(in: &webViewObservers) | |
webView.publisher(for: \.title, options: .initial) | |
.receive(on: RunLoop.main) | |
.sink { [weak self] in | |
guard self?.parent?.content.title != $0 else { return } | |
self?.parent?.content.title = $0 | |
} | |
.store(in: &webViewObservers) | |
webView.publisher(for: \.isLoading, options: .initial) | |
.receive(on: RunLoop.main) | |
.sink { [weak self] in | |
guard self?.parent?.content.isLoading != $0 else { return } | |
self?.parent?.content.isLoading = $0 | |
} | |
.store(in: &webViewObservers) | |
webView.publisher(for: \.canGoBack, options: .initial) | |
.receive(on: RunLoop.main) | |
.sink { [weak self] in | |
guard self?.parent?.content.canGoBack != $0 else { return } | |
self?.parent?.content.canGoBack = $0 | |
} | |
.store(in: &webViewObservers) | |
webView.publisher(for: \.canGoForward, options: .initial) | |
.receive(on: RunLoop.main) | |
.sink { [weak self] in | |
guard self?.parent?.content.canGoForward != $0 else { return } | |
self?.parent?.content.canGoForward = $0 | |
} | |
.store(in: &webViewObservers) | |
} | |
} | |
} | |
} | |
extension WebView.Coordinator: WKNavigationDelegate { | |
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { | |
// print("didFinish", navigation) | |
} | |
@MainActor | |
func webView( | |
_ webView: WKWebView, | |
decidePolicyFor navigationAction: WKNavigationAction, | |
preferences: WKWebpagePreferences | |
) async -> (WKNavigationActionPolicy, WKWebpagePreferences) { | |
// print("decidePolicyFor", navigationAction) | |
#if os(iOS) | |
if | |
let url = navigationAction.request.url, | |
navigationAction.targetFrame?.isMainFrame == true, | |
url.host() != "clever.com" | |
{ | |
let isHTTP = url.scheme == "http" || url.scheme == "https" | |
if await UIApplication.shared.open(url, options: [.universalLinksOnly: isHTTP]) { | |
print("opend in app", url) | |
return (.cancel, preferences) | |
} | |
} | |
#endif | |
return (.allow, preferences) | |
} | |
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) { | |
print("didCommit", webView, navigation) | |
} | |
func webView(_ webView: WKWebView, respondTo challenge: URLAuthenticationChallenge) async -> (URLSession.AuthChallengeDisposition, URLCredential?) { | |
// print("respondTo", challenge) | |
return (.useCredential, nil) | |
} | |
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { | |
// print("didStartProvisionalNavigation", navigation) | |
} | |
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { | |
print("didFail", webView, navigation, error) | |
} | |
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { | |
print("didFailProvisionalNavigation", webView, navigation, error) | |
} | |
} | |
extension WebView.Coordinator: WKUIDelegate { | |
func webView( | |
_ webView: WKWebView, | |
createWebViewWith configuration: WKWebViewConfiguration, | |
for navigationAction: WKNavigationAction, | |
windowFeatures: WKWindowFeatures | |
) -> WKWebView? { | |
print("createWebViewWith", webView, configuration, navigationAction, windowFeatures) | |
if navigationAction.targetFrame?.isMainFrame != true { | |
webView.load(navigationAction.request) | |
} | |
return nil | |
} | |
func webViewDidClose(_ webView: WKWebView) { | |
print("webViewDidClose", webView) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment