Skip to content

Instantly share code, notes, and snippets.

@davbeck
Created December 1, 2023 20:22
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save davbeck/2edad5bc555ef2867528a89f1288d5a9 to your computer and use it in GitHub Desktop.
Save davbeck/2edad5bc555ef2867528a89f1288d5a9 to your computer and use it in GitHub Desktop.
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