Skip to content

Instantly share code, notes, and snippets.

@BAProductions
Last active April 4, 2023 12:41
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save BAProductions/eebc7cc458f9d917d49d52c6dc90b906 to your computer and use it in GitHub Desktop.
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.
//
// 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