Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Implement webkit with swiftUI on macOS (And create a preview of a webpage)

I'm trying to create a menubar application which shows the inbox of this site. I would like to make an easy function that opens a small popup with the url of the item (without opening safari). An inbox item would look something like this

struct InboxItem: View {
    @State var MesgSite: String = "https://duckduckgo.com"
    @State private var showSafari = false    
   
    var body: some View {
        VStack(alignment: .leading) {
            Text("some text")
        .background(SelectColor.opacity(0.5))
        .onLongPressGesture {
           //show preview of the MesgSite here
            self.SelectColor = .blue
            self.showSafari.toggle()
        }.popover(isPresented: self.$showSafari) {
            SafariPreview()
        }
    }
}

struct SafariPreview: View {

    var body: some View {
        VStack {
            Text("Display the webpage here")
            .padding()
        }.frame(maxWidth: 533, maxHeight: 300)
    }

}

I would like to, when one longpresses on the item, it should make a preview of the associated webpage just like in the default mail app on macOS like so:

Got the popup working now

Open a preview of the website

I have tried adding a wkwebview as well as a SafariView in a NSViewRepresentable, however I got (among similar) the following error message using code from this SO post

Use of undeclared type 'UIViewRepresentable'

TIA!

Edit:

The most basic version of the project can be found here on github

Edit 2:

Gave more focus to the question

like image 791
Chiel Avatar asked Jul 17 '20 21:07

Chiel


2 Answers

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:

macOS Safari webpage preview with swiftUI

like image 169
Chiel Avatar answered Nov 15 '22 09:11

Chiel


Answer of Chiel is great!

But, possibly, someone search for html renderer view without full browser features

WORKS FOR MACOS

import SwiftUI
import WebKit

struct WebView: View {
    @Binding var html: String
    
    var body: some View {
        WebViewWrapper(html: html)
    }
}

struct WebViewWrapper: NSViewRepresentable {
    let html: String
    
    func makeNSView(context: Context) -> WKWebView {
        return WKWebView()
    }
    
    func updateNSView(_ nsView: WKWebView, context: Context) {
        nsView.loadHTMLString(html, baseURL: nil)
    }
}

usage:

@State var text = "<html><body><h1>Hello World</h1><p><strong>hell</strong> yeah!</p></body></html>"
//......

TextField("", text: $text)
        
Divider()

WebView(html: $text)
    .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)

enter image description here

like image 32
Andrew Avatar answered Nov 15 '22 08:11

Andrew