I have also tried to come up with a solution. Since I couldn't find any documentation on this online whatsoever I'll give the solution that I've found by trial and error.
First, as it turns out UI...
has its counterpart on macOS called NS...
. Thus UIViewRepresentable
would be NSViewRepresentable
on macOS. Next I found this SO question which had an example of a WKWebview
on macOS. By combining that code with this answer on another SO question I could also detect the url change as well know when the view was done loading.
The SwiftUI WebView on macOS
This resulted in the following code. For clarity, I suggest putting it in a different file like WebView.swift
:
First, import the needed packages:
import SwiftUI
import WebKit
import Combine
Then create a model that holds the data that you want to be able to access in your SwiftUI views:
class WebViewModel: ObservableObject {
@Published var link: String
@Published var didFinishLoading: Bool = false
@Published var pageTitle: String
init (link: String) {
self.link = link
self.pageTitle = ""
}
}
Lastly, create the struct
with NSViewRepresentable
that will be the HostingViewController of the WebView()
like so:
struct SwiftUIWebView: NSViewRepresentable {
public typealias NSViewType = WKWebView
@ObservedObject var viewModel: WebViewModel
private let webView: WKWebView = WKWebView()
public func makeNSView(context: NSViewRepresentableContext<SwiftUIWebView>) -> WKWebView {
webView.navigationDelegate = context.coordinator
webView.uiDelegate = context.coordinator as? WKUIDelegate
webView.load(URLRequest(url: URL(string: viewModel.link)!))
return webView
}
public func updateNSView(_ nsView: WKWebView, context: NSViewRepresentableContext<SwiftUIWebView>) { }
public func makeCoordinator() -> Coordinator {
return Coordinator(viewModel)
}
class Coordinator: NSObject, WKNavigationDelegate {
private var viewModel: WebViewModel
init(_ viewModel: WebViewModel) {
//Initialise the WebViewModel
self.viewModel = viewModel
}
public func webView(_: WKWebView, didFail: WKNavigation!, withError: Error) { }
public func webView(_: WKWebView, didFailProvisionalNavigation: WKNavigation!, withError: Error) { }
//After the webpage is loaded, assign the data in WebViewModel class
public func webView(_ web: WKWebView, didFinish: WKNavigation!) {
self.viewModel.pageTitle = web.title!
self.viewModel.link = web.url?.absoluteString as! String
self.viewModel.didFinishLoading = true
}
public func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { }
public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
decisionHandler(.allow)
}
}
}
This code can be used as follows:
struct ContentView: View {
var body: some View {
//Pass the url to the SafariWebView struct.
SafariWebView(mesgURL: "https://stackoverflow.com/")
}
}
struct SafariWebView: View {
@ObservedObject var model: WebViewModel
init(mesgURL: String) {
//Assign the url to the model and initialise the model
self.model = WebViewModel(link: mesgURL)
}
var body: some View {
//Create the WebView with the model
SwiftUIWebView(viewModel: model)
}
}
Create a Safari Preview
So now we have this knowledge it is relatively easy to recreate the handy safari preview.
To have that, make sure to add @State private var showSafari = false
(which will be toggled when you want to show the preview) to the view that will call the preview.
Also add the .popover(isPresented: self.$showSafari) { ...
to show the preview
struct ContentView: View {
@State private var showSafari = false
var body: some View {
VStack(alignment: .leading) {
Text("Press me to get a preview")
.padding()
}
.onLongPressGesture {
//Toggle to showSafari preview
self.showSafari.toggle()
}//if showSafari is true, create a popover
.popover(isPresented: self.$showSafari) {
//The view inside the popover is made of the SafariPreview
SafariPreview(mesgURL: "https://duckduckgo.com/")
}
}
}
Now the SafariPreview struct
will look like this:
struct SafariPreview: View {
@ObservedObject var model: WebViewModel
init(mesgURL: String) {
self.model = WebViewModel(link: mesgURL)
}
var body: some View {
//Create a VStack that contains the buttons in a preview as well a the webpage itself
VStack {
HStack(alignment: .center) {
Spacer()
Spacer()
//The title of the webpage
Text(self.model.didFinishLoading ? self.model.pageTitle : "")
Spacer()
//The "Open with Safari" button on the top right side of the preview
Button(action: {
if let url = URL(string: self.model.link) {
NSWorkspace.shared.open(url)
}
}) {
Text("Open with Safari")
}
}
//The webpage itself
SwiftUIWebView(viewModel: model)
}.frame(width: 800, height: 450, alignment: .bottom)
.padding(5.0)
}
}
The result looks like this:
