Wkwebview on Macos Cuts Off Top

SwiftUI: WKWebView Cutting Off Page Content on macOS (NSViewRepresentable)

I had the exact same issue with WKWebView in MacOS app using SwiftUI.

The solution that worked for me is to use GeometryReader to get the exact height, and put the web view inside a scrollview (I believe it has something to do with the layout priority calculation, but couldn't get to the core of it yet).

Here is a snippet of what worked for me, maybe it will work with you as well

GeometryReader { g in
ScrollView {
BrowserView().tabItem {
Text("Browser")
}
.frame(height: g.size.height)
.tag(1)

}.frame(height: g.size.height)
}

SwiftUI WKWebView content height issue

It is confusing of ScrollView in SwiftUI, which expects known content size in advance, and UIWebView internal UIScrollView, which tries to get size from parent view... cycling.

So here is possible approach.. to pass determined size from web view into SwiftUI world, so no hardcoding is used and ScrollView behaves like having flat content.

At first demo of result, as I understood and simulated ...

Sample Image

Here is complete module code of demo. Tested & worked on Xcode 11.2 / iOS 13.2.

import SwiftUI
import WebKit

struct Webview : UIViewRepresentable {
@Binding var dynamicHeight: CGFloat
var webview: WKWebView = WKWebView()

class Coordinator: NSObject, WKNavigationDelegate {
var parent: Webview

init(_ parent: Webview) {
self.parent = parent
}

func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
webView.evaluateJavaScript("document.documentElement.scrollHeight", completionHandler: { (height, error) in
DispatchQueue.main.async {
self.parent.dynamicHeight = height as! CGFloat
}
})
}
}

func makeCoordinator() -> Coordinator {
Coordinator(self)
}

func makeUIView(context: Context) -> WKWebView {
webview.scrollView.bounces = false
webview.navigationDelegate = context.coordinator
let htmlStart = "<HTML><HEAD><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, shrink-to-fit=no\"></HEAD><BODY>"
let htmlEnd = "</BODY></HTML>"
let dummy_html = """
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec ut venenatis risus. Fusce eget orci quis odio lobortis hendrerit. Vivamus in sollicitudin arcu. Integer nisi eros, hendrerit eget mollis et, fringilla et libero. Duis tempor interdum velit. Curabitur</p>
<p>ullamcorper, nulla nec elementum sagittis, diam odio tempus erat, at egestas nibh dui nec purus. Suspendisse at risus nibh. Mauris lacinia rutrum sapien non faucibus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec interdum enim et augue suscipit, vitae mollis enim maximus.</p>
<p>Fusce et convallis ligula. Ut rutrum ipsum laoreet turpis sodales, nec gravida nisi molestie. Ut convallis aliquet metus, sit amet vestibulum risus dictum mattis. Sed nec leo vel mauris pharetra ornare quis non lorem. Aliquam sed justo</p>
"""
let htmlString = "\(htmlStart)\(dummy_html)\(htmlEnd)"
webview.loadHTMLString(htmlString, baseURL: nil)
return webview
}

func updateUIView(_ uiView: WKWebView, context: Context) {
}
}

struct TestWebViewInScrollView: View {
@State private var webViewHeight: CGFloat = .zero
var body: some View {
ScrollView {
VStack {
Image(systemName: "doc")
.resizable()
.scaledToFit()
.frame(height: 300)
Divider()
Webview(dynamicHeight: $webViewHeight)
.padding(.horizontal)
.frame(height: webViewHeight)
}
}
}
}

struct TestWebViewInScrollView_Previews: PreviewProvider {
static var previews: some View {
TestWebViewInScrollView()
}
}

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

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

WKWebView in Interface Builder

You are correct - it doesn't seem to work. If you look in the headers, you'll see:

- (instancetype)initWithCoder:(NSCoder *)coder NS_UNAVAILABLE;

which implies that you can't instantiate one from a nib.

You'll have to do it by hand in viewDidLoad or loadView.

How do I get the file open dialog to work, when called through a SwiftUI WKWebView inside a macOS native app?

You have to implement the following WKUIDelegate delegate method

   /** @abstract Displays a file upload panel.
@param webView The web view invoking the delegate method.
@param parameters Parameters describing the file upload control.
@param frame Information about the frame whose file upload control initiated this call.
@param completionHandler The completion handler to call after open panel has been dismissed. Pass the selected URLs if the user chose OK, otherwise nil.

If you do not implement this method, the web view will behave as if the user selected the Cancel button.
*/
@available(OSX 10.12, *)
optional func webView(_ webView: WKWebView, runOpenPanelWith
parameters: WKOpenPanelParameters, initiatedByFrame frame: WKFrameInfo,
completionHandler: @escaping ([URL]?) -> Void)

Here is example of implementation

func webView(_ webView: WKWebView, runOpenPanelWith parameters: WKOpenPanelParameters, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping ([URL]?) -> Void) {
let openPanel = NSOpenPanel()
openPanel.canChooseFiles = true
openPanel.begin { (result) in
if result == NSApplication.ModalResponse.OK {
if let url = openPanel.url {
completionHandler([url])
}
} else if result == NSApplication.ModalResponse.cancel {
completionHandler(nil)
}
}
}


Related Topics



Leave a reply



Submit