import AppKit import SwiftUI import WebKit struct RedditWebView: NSViewRepresentable { let url: URL let reloadTrigger: Int func makeCoordinator() -> Coordinator { Coordinator() } func makeNSView(context: Context) -> RedditWebViewContainer { let container = RedditWebViewContainer(webView: RedditWebViewManager.makeWebView()) container.webView.navigationDelegate = context.coordinator container.webView.uiDelegate = context.coordinator context.coordinator.container = container context.coordinator.lastReloadTrigger = reloadTrigger container.load(url: url) return container } func updateNSView(_ container: RedditWebViewContainer, context: Context) { guard context.coordinator.lastReloadTrigger != reloadTrigger else { return } context.coordinator.lastReloadTrigger = reloadTrigger container.load(url: url) } final class Coordinator: NSObject, WKNavigationDelegate, WKUIDelegate { weak var container: RedditWebViewContainer? var lastReloadTrigger = -1 func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { webView.evaluateJavaScript(RedditPostBoundaryStyle.injectionScript, completionHandler: nil) } func webViewWebContentProcessDidTerminate(_ webView: WKWebView) { container?.reloadLastURL() } func webView( _ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error ) { let nsError = error as NSError guard nsError.domain != NSURLErrorDomain || nsError.code != NSURLErrorCancelled else { return } container?.reloadLastURL(after: 1.0) } func webView( _ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void ) { guard let url = navigationAction.request.url else { decisionHandler(.allow) return } if RedditWebAuthHelper.isOAuthProviderURL(url) { beginOAuthSignIn(url: url) decisionHandler(.cancel) return } if navigationAction.targetFrame == nil { webView.load(URLRequest(url: url)) decisionHandler(.cancel) return } if navigationAction.navigationType == .linkActivated, !RedditWebAuthHelper.isRedditURL(url) { NSWorkspace.shared.open(url) decisionHandler(.cancel) return } decisionHandler(.allow) } func webView( _ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures ) -> WKWebView? { guard let url = navigationAction.request.url else { return nil } if RedditWebAuthHelper.isOAuthProviderURL(url) { beginOAuthSignIn(url: url) return nil } webView.load(URLRequest(url: url)) return nil } private func beginOAuthSignIn(url: URL) { let presentationWindow = container?.window OAuthAuthenticationManager.shared.authenticate( url: url, presentationWindow: presentationWindow ) { [weak self] result in guard let self, let container = self.container else { return } switch result { case .success(let callbackURL): RedditOAuthPanelController.shared.completeSignIn(callbackURL: callbackURL) { container.load(url: RedditWebAuthHelper.redditHomeURL) } case .failure(let error as OAuthError) where error == .cancelled: break case .failure: break } } } } } final class RedditWebViewContainer: NSView { let webView: WKWebView private var pendingURL: URL? private var lastLoadedURL: URL? private var reloadWorkItem: DispatchWorkItem? init(webView: WKWebView) { self.webView = webView super.init(frame: .zero) wantsLayer = true addSubview(webView) webView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ webView.leadingAnchor.constraint(equalTo: leadingAnchor), webView.trailingAnchor.constraint(equalTo: trailingAnchor), webView.topAnchor.constraint(equalTo: topAnchor), webView.bottomAnchor.constraint(equalTo: bottomAnchor), ]) } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func load(url: URL) { pendingURL = url lastLoadedURL = url reloadWorkItem?.cancel() tryLoadIfReady() } func reloadLastURL(after delay: TimeInterval = 0) { guard let lastLoadedURL else { return } reloadWorkItem?.cancel() let workItem = DispatchWorkItem { [weak self] in self?.load(url: lastLoadedURL) } reloadWorkItem = workItem DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: workItem) } override func layout() { super.layout() tryLoadIfReady() } override func viewDidMoveToWindow() { super.viewDidMoveToWindow() tryLoadIfReady() } private func tryLoadIfReady() { guard let url = pendingURL else { return } guard bounds.width > 1, bounds.height > 1 else { return } pendingURL = nil webView.load(URLRequest(url: url)) } }