Last active
April 4, 2023 12:41
-
-
Save BAProductions/eebc7cc458f9d917d49d52c6dc90b906 to your computer and use it in GitHub Desktop.
SwiftUI Wrapper WKWebView works, But still think it can be better optmize if anyone wants optmize better feel free to do so.
This file contains 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
// | |
// webview.swift | |
// BR | |
// | |
// Created by BAproductions on 8/12/22. | |
// | |
import SwiftUI | |
import WebKit | |
import Combine | |
#if os(macOS) | |
struct SwiftUIWebView: NSViewRepresentable { | |
@Binding var url: String | |
@State private var mWKWebView: WKWebView = WKWebView(frame: .zero) | |
@AppStorage("fontSize") private var fontSize: Double? | |
@AppStorage("allowsLinkPreview") private var allowsLinkPreview: Bool = true | |
@AppStorage("allowsMagnification") private var allowsMagnification: Bool = true | |
@AppStorage("allowsBackForwardNavigationGestures") private var allowsBackForwardNavigationGestures: Bool = true | |
@AppStorage("allowedTouchTypes") private var allowedTouchTypes: NSTouch.TouchType = .direct | |
@AppStorage("allowsAirPlayForMediaPlayback") private var allowsAirPlayForMediaPlayback: Bool = true | |
@AppStorage("limitsNavigationsToAppBoundDomains") private var limitsNavigationsToAppBoundDomains: Bool = false | |
@AppStorage("upgradeKnownHostsToHTTPS") private var upgradeKnownHostsToHTTPS: Bool = false | |
@AppStorage("suppressesIncrementalRendering") private var suppressesIncrementalRendering: Bool = true | |
@AppStorage("mediaTypesRequiringUserActionForPlayback") private var mediaTypesRequiringUserActionForPlayback: Int = 0 | |
@AppStorage("javaScriptCanOpenWindowsAutomatically") private var javaScriptCanOpenWindowsAutomatically: Bool = true | |
@AppStorage("fraudulentWebsiteWarningEnabled") private var fraudulentWebsiteWarningEnabled: Bool = true | |
@AppStorage("tabFocusesLinks") private var tabFocusesLinks: Bool = true | |
@AppStorage("siteSpecificQuirksModeEnabled") private var siteSpecificQuirksModeEnabled: Bool = true | |
@AppStorage("textInteractionEnabled") private var textInteractionEnabled: Bool = true | |
@AppStorage("elementFullscreenEnabled") private var elementFullscreenEnabled: Bool = true | |
@AppStorage("developerExtrasEnabled") private var developerExtrasEnabled: Bool = true | |
@AppStorage("allowsContentJavaScript") private var allowsContentJavaScript: Bool = true | |
@AppStorage("preferredContentMode") private var preferredContentMode: WKWebpagePreferences.ContentMode = .recommended | |
@AppStorage("lockdownModeEnabled") private var lockdownModeEnabled: Bool = false | |
@AppStorage("canGoBack") private var canGoBack: Bool = false | |
@AppStorage("canGoForward") private var canGoForward: Bool = false | |
private var configuration = WKWebViewConfiguration() | |
@State private var v: Double = 0.0 | |
@ObservedObject var viewModel: SwiftUIWebViewModel | |
init(url: Binding<String>, viewModel: SwiftUIWebViewModel) { | |
_url = url // this is how you pass a binding in the init, using the "_" syntax | |
self.viewModel = viewModel | |
} | |
var request: URLRequest { | |
get{ | |
let request: URLRequest | |
let url: URL = url.setURL() | |
if url.isReachable() { | |
request = URLRequest(url: url,cachePolicy: .useProtocolCachePolicy, timeoutInterval: 10) | |
} else { | |
request = URLRequest(url: url,cachePolicy: .returnCacheDataElseLoad, timeoutInterval: 60) | |
} | |
return request | |
} | |
} | |
func makeNSView(context: Context) -> WKWebView { | |
self.mWKWebView.navigationDelegate = context.coordinator | |
self.mWKWebView.uiDelegate = context.coordinator | |
if !mWKWebView.isLoading { | |
DispatchQueue.global(qos: .background).async { | |
DispatchQueue.main.async { | |
mWKWebView.load(request) | |
} | |
} | |
} | |
return mWKWebView | |
} | |
func updateNSView(_ view: WKWebView, context: Context) { | |
view.allowsLinkPreview = allowsLinkPreview | |
view.allowsMagnification = allowsMagnification | |
view.allowsBackForwardNavigationGestures = allowsBackForwardNavigationGestures | |
view.allowedTouchTypes = getTouchType() | |
view.configuration.allowsAirPlayForMediaPlayback = allowsAirPlayForMediaPlayback | |
view.configuration.limitsNavigationsToAppBoundDomains = limitsNavigationsToAppBoundDomains | |
view.configuration.upgradeKnownHostsToHTTPS = upgradeKnownHostsToHTTPS | |
view.configuration.suppressesIncrementalRendering = suppressesIncrementalRendering | |
view.configuration.applicationNameForUserAgent = Bundle.main.infoDictionary?["CFBundleName"] as? String | |
view.configuration.mediaTypesRequiringUserActionForPlayback = getWKAudiovisualMediaTypes() | |
view.evaluateJavaScript("navigator.userAgent") { (result, error) in | |
guard let userAgent = result as? String else { | |
return | |
} | |
let version = "Version/\(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown")" | |
let customUserAgent = userAgent.replacingOccurrences(of: "Chrome", with: "MyApp").replacingOccurrences(of: "Intel Mac OS X 10_15_7", with: version) | |
view.customUserAgent = customUserAgent | |
} | |
view.configuration.preferences.javaScriptCanOpenWindowsAutomatically = !javaScriptCanOpenWindowsAutomatically | |
view.configuration.preferences.isFraudulentWebsiteWarningEnabled = fraudulentWebsiteWarningEnabled | |
view.configuration.preferences.tabFocusesLinks = !tabFocusesLinks | |
view.configuration.preferences.isSiteSpecificQuirksModeEnabled = siteSpecificQuirksModeEnabled | |
view.configuration.preferences.isTextInteractionEnabled = textInteractionEnabled | |
view.configuration.preferences.isElementFullscreenEnabled = !elementFullscreenEnabled | |
view.configuration.preferences.setValue(developerExtrasEnabled, forKey: "developerExtrasEnabled") | |
view.configuration.defaultWebpagePreferences.allowsContentJavaScript = !allowsContentJavaScript | |
view.configuration.defaultWebpagePreferences.preferredContentMode = preferredContentMode | |
view.configuration.defaultWebpagePreferences.isLockdownModeEnabled = lockdownModeEnabled | |
view.configuration.preferences.minimumFontSize = fontSize ?? 14 | |
if !view.isLoading { | |
DispatchQueue.global(qos: .background).async { | |
DispatchQueue.main.async { | |
self.viewModel.updateCanNavgate(view.canGoBack, view.canGoForward) | |
} | |
} | |
} | |
} | |
func webViewDidStartLoad(_ webView: WKWebView) { | |
} | |
func webViewDidFinishLoad(_ webView: WKWebView) { | |
} | |
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { | |
viewModel.progress = 0.1 | |
} | |
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { | |
viewModel.progress = 1.0 | |
} | |
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { | |
viewModel.progress = 1.0 | |
} | |
func goTo(newURL:String = "") { | |
var newRequest: URLRequest { | |
let request: URLRequest | |
let url: URL = newURL.setURL() | |
if url.isReachable() { | |
request = URLRequest(url: url,cachePolicy: .useProtocolCachePolicy, timeoutInterval: 10) | |
} else { | |
request = URLRequest(url: url,cachePolicy: .returnCacheDataElseLoad, timeoutInterval: 60) | |
} | |
return request | |
} | |
mWKWebView.load(newRequest) | |
print("Heading To") | |
} | |
func refresh() { | |
mWKWebView.reload() | |
print("Reloading") | |
} | |
func goBack() { | |
mWKWebView.goBack() | |
print("Going Backwards") | |
} | |
func goForward() { | |
mWKWebView.goForward() | |
print("Going Forwards") | |
} | |
func isLoaded() -> Bool { | |
return mWKWebView.isLoading | |
} | |
private func getTouchType() -> NSTouch.TouchTypeMask { | |
var touchTypeMask: NSTouch.TouchTypeMask | |
switch(allowedTouchTypes) { | |
case.direct: | |
touchTypeMask = .direct | |
default: | |
touchTypeMask = .indirect | |
} | |
return touchTypeMask | |
} | |
private func getWKAudiovisualMediaTypes() -> WKAudiovisualMediaTypes { | |
var wKAudiovisualMediaTypes: WKAudiovisualMediaTypes | |
switch(mediaTypesRequiringUserActionForPlayback) { | |
case 1: | |
wKAudiovisualMediaTypes = .audio | |
case 2: | |
wKAudiovisualMediaTypes = .video | |
default: | |
wKAudiovisualMediaTypes = .all | |
} | |
return wKAudiovisualMediaTypes | |
} | |
func makeCoordinator() -> Coordinator { | |
Coordinator(self, viewModel: viewModel) | |
} | |
class Coordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler, WKUIDelegate, NSWindowDelegate { | |
private var parent: SwiftUIWebView | |
private var viewModel: SwiftUIWebViewModel | |
private var observer: NSKeyValueObservation? | |
init(_ parent: SwiftUIWebView, viewModel: SwiftUIWebViewModel) { | |
self.parent = parent | |
self.viewModel = viewModel | |
super.init() | |
observer = self.parent.mWKWebView.observe(\.estimatedProgress) { [weak self] webView, _ in | |
guard let self = self else { return } | |
DispatchQueue.global(qos: .background).async { [weak self] in | |
DispatchQueue.main.async { | |
self?.parent.viewModel.updateProgress(webView.estimatedProgress) | |
} | |
} | |
} | |
} | |
deinit { | |
observer = nil | |
} | |
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { | |
if message.name == "closeWindow" { | |
self.parent.mWKWebView.evaluateJavaScript("document.querySelector('video').pause();") { result, error in | |
} | |
} | |
} | |
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { | |
if let url = webView.url { | |
DispatchQueue.global(qos: .background).async { [weak self] in | |
DispatchQueue.main.async { | |
self?.parent.viewModel.updateUrl(url.description) | |
self?.parent.viewModel.updateCanNavgate(webView.canGoBack, webView.canGoForward) | |
} | |
} | |
} | |
} | |
func webView(_ webView: WKWebView, didReceiveServerRedirectForProvisionalNavigation navigation: WKNavigation!) { | |
if !webView.isLoading { | |
DispatchQueue.global(qos: .background).async { [weak self] in | |
DispatchQueue.main.async { | |
webView.load((self?.parent.request)!) | |
self?.parent.viewModel.updateCanNavgate(webView.canGoBack, webView.canGoForward) | |
} | |
} | |
} | |
webView.reload() // Reload the current webpage | |
print("Web view navigation commit successfully.") | |
} | |
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { | |
print("Web view navigation failed: \(error.localizedDescription)") | |
} | |
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { | |
print("Web view navigation failed: \(error.localizedDescription)") | |
} | |
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { | |
print("Web view navigation finished successfully.") | |
// Inject JavaScript code to send a message to close the window when the video ends | |
let source = "document.querySelector('video').addEventListener('ended', function() { window.webkit.messageHandlers.closeWindow.postMessage(null); });" | |
let script = WKUserScript(source: source, injectionTime: .atDocumentEnd, forMainFrameOnly: true) | |
webView.configuration.userContentController.addUserScript(script) | |
} | |
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) { | |
if !webView.isLoading { | |
DispatchQueue.global(qos: .background).async { [weak self] in | |
DispatchQueue.main.async { | |
self?.parent.viewModel.updateCanNavgate(webView.canGoBack, webView.canGoForward) | |
} | |
} | |
} | |
print("Web view navigation commit successfully.") | |
} | |
func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { | |
completionHandler(.performDefaultHandling, nil) | |
} | |
func webViewWebContentProcessDidTerminate(_ webView: WKWebView) { | |
webView.reload() | |
} | |
func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? { | |
if navigationAction.targetFrame == nil { | |
let newWindow = NSWindow(contentRect: NSRect(x: windowFeatures.x as? Int ?? 0, y: windowFeatures.y as? Int ?? 0, width: windowFeatures.width as? Int ?? 800, height: windowFeatures.height as? Int ?? 800), styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView], backing: .buffered, defer: true) | |
newWindow.titlebarSeparatorStyle = .none | |
newWindow.toolbarStyle = .unifiedCompact | |
newWindow.titlebarAppearsTransparent = true | |
newWindow.allowsToolTipsWhenApplicationIsInactive = true | |
newWindow.allowsConcurrentViewDrawing = true | |
newWindow.acceptsMouseMovedEvents = true | |
newWindow.isReleasedWhenClosed = true | |
newWindow.showsResizeIndicator = windowFeatures.allowsResizing == 1 ? true : false | |
let swiftUIView = WebView(URL: navigationAction.request.url?.description ?? "", showToolBar: windowFeatures.toolbarsVisibility == 1 ? true : false ) | |
let hostingView = NSHostingView(rootView: swiftUIView) | |
newWindow.contentView = hostingView | |
newWindow.makeKeyAndOrderFront(nil) | |
return nil | |
} | |
return nil | |
} | |
} | |
} | |
extension SwiftUIWebView { | |
class SwiftUIWebViewModel: ObservableObject { | |
private var queue = DispatchQueue(label: "com.example.progressViewModelQueue") | |
@Published var progress: Double = 0.0 | |
@Published var url: String = "" | |
@Published var canGoBack: Bool = false | |
@Published var canGoForward: Bool = false | |
init(progress: Double, url: String = "", canGoBack: Bool = false, canGoForward: Bool = false) { | |
queue.sync { | |
self.progress = progress | |
} | |
} | |
func updateProgress(_ progress: Double) { | |
queue.sync { | |
self.progress = progress | |
} | |
} | |
func updateUrl(_ url: String) { | |
queue.sync { | |
self.url = url | |
} | |
} | |
func updateCanNavgate(_ canGoBack: Bool, _ canGoForward: Bool) { | |
queue.sync { | |
self.canGoBack = canGoBack | |
self.canGoForward = canGoForward | |
} | |
} | |
} | |
func getUserAgent() -> String { | |
let osVersion = ProcessInfo.processInfo | |
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "" | |
let osNameVersion = "\(osVersion.operatingSystemVersionString) \(osVersion.operatingSystemVersion)" | |
let webKitVersion = "\(WKWebView().value(forKey: "version") ?? "")" | |
return "Mozilla/5.0 (\(osNameVersion)) AppleWebKit/\(webKitVersion) (KHTML, like Gecko) MyAwesomeApp/\(appVersion) Safari/\(webKitVersion)" | |
} | |
} | |
#endif | |
extension String { | |
func setURL() -> URL { | |
if let url:URL = URL(string: self) { | |
return url | |
} else { | |
return URL(fileURLWithPath: "STANDARD-PC-Q35-ICH9-2009-EC852729._smb._tcp.local/se/Pictures/43129154_054_fa64.jpg") | |
} | |
} | |
} | |
extension URL { | |
func isReachable() -> Bool { | |
do { | |
if try self.checkResourceIsReachable() { | |
return true | |
} else { | |
return false | |
} | |
} catch { | |
return false | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment