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 ...
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:
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
Getting an Ip Address and Port Number from a Sockaddr_In Struct in Swift
Validate Unicode Code Point in Swift
How to Pass Data Backwards to a View Controller After Passing Forward
How to Apply Impulse to The Node on Touch Angle
Wkwebview - Update HTML Tags from Swiftui Textfields
How to Convert Curl Request to Swift Using Alamofire
How to Load Lcr Image in Tvos Apps
How to Hide The Top Bar (With Buttons) Usin Swift and Macos
Swift Framework with Opencv Dynamic Framework, Library Not Loaded
How to Have a Searchbar Which Shows Suggestions with Different UItableview
How to Assign Elements of a Dictionary to JSON Object in Vapor 3
Swiftui Navigationview Starting Inside Itself
How to Drag a Working Slider Using Swiftui
How to Run Terminal Command in Swift from Any Directory
Uialertview or UIalertcontroller to Display Only Once in Swift