12

I have a basic NSViewRepresentable implementation of WKWebView, for use with SwiftUI apps on macOS. The UIViewRepresentable equivalent works fine on iOS, but on macOS (natively, not Catalyst), the top content is always cut off.

The amount lost always equals the size of parent views (such as the tab view) and their padding, which indicates that the web view keeps scaling its content to the window size, rather than the view size.

For example, this page:

Cut Off Web Content

...should be as follows (as shown in Chrome). The entire navigation bar has been cropped out (though the sides appear not to be affected).

Chrome Version Rendering Correctly

Any suggestions on how to fix this? Interestingly, if I switch back & forth between tabs, the content shows correctly for ~1 second, then resizes the content so it's cut off again. This makes me think something's required in the updateNSView method, but I'm not sure what.

Seems to be a similar issue to the one discussed here, but that's for IB-based apps, and I can't see a way to apply it for SwiftUI.

The code used is as follows. Note: The web view is kept as a property so it can be referenced by other methods (such as triggering page load, refresh, go back, etc.)

public struct WebBrowserView {

    private let webView: WKWebView = WKWebView()

    // ...

    public func load(url: URL) {        
        webView.load(URLRequest(url: url))
    }

    public class Coordinator: NSObject, WKNavigationDelegate, WKUIDelegate {

        var parent: WebBrowserView

        init(parent: WebBrowserView) {
            self.parent = parent
        }

        public func webView(_: WKWebView, didFail: WKNavigation!, withError: Error) {
            // ...
        }

        public func webView(_: WKWebView, didFailProvisionalNavigation: WKNavigation!, withError: Error) {
            // ...
        }

        public func webView(_: WKWebView, didFinish: WKNavigation!) {
            // ...
        }

        public func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
            // ...
        }

        public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
            decisionHandler(.allow)
        }

        public func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
            if navigationAction.targetFrame == nil {
                webView.load(navigationAction.request)
            }
            return nil
        }
    }

    public func makeCoordinator() -> Coordinator {
        Coordinator(parent: self)
    }
}


#if os(macOS) // macOS Implementation (iOS version omitted for brevity)
extension WebBrowserView: NSViewRepresentable {

    public typealias NSViewType = WKWebView

    public func makeNSView(context: NSViewRepresentableContext<WebBrowserView>) -> WKWebView {

        webView.navigationDelegate = context.coordinator
        webView.uiDelegate = context.coordinator
        return webView
    }

    public func updateNSView(_ nsView: WKWebView, context: NSViewRepresentableContext<WebBrowserView>) {

    }
}
#endif

Example usage:

struct BrowserView: View {

    private let browser = WebBrowserView()

    var body: some View {
        HStack {
            browser
                .onAppear() {
                    self.browser.load(url: URL(string: "https://stackoverflow.com/tags")!)
                }
        }
        .padding()
    }
}

struct ContentView: View {

    @State private var selection = 0

    var body: some View { 
        TabView(selection: $selection){
            Text("Email View")
                .tabItem {
                    Text("Email")
                }
                .tag(0)
            BrowserView()
                .tabItem {
                    Text("Browser")
                }
                .tag(1)
        }
        .padding()
    }
}
TheNeil
  • 3,321
  • 2
  • 27
  • 52
  • Testing out your code, I am getting "Value of type 'WebBrowserView' has no member 'load'. What am I doing wrong? I pasted all of this into ContentView.swift? – esaruoho Jan 16 '20 at 19:00
  • Oops! I had stripped out a little too much code for brevity. I've edited it to add that method back in. Please try now and let me know if you have any other issues. Thanks for letting me know! – TheNeil Jan 17 '20 at 00:05
  • Hi, thanks for the fixes @TheNeil - I added both of these bits of code to ViewController.swift and got: "Use of undeclared type 'NSViewRepresentable'", "Use of undeclared type 'NSViewRepresentableContext'" , "Use of undeclared type 'View'", Unknown attribute 'State', errors. I'm not putting them in the right places, am I? – esaruoho Jan 17 '20 at 06:28
  • Thanks for the further follow up. Because these are SwiftUI views, they'd need to go into the view of a SwiftUI-based app (such as the `ContentView` above). If it's saying `NSViewRepresentable` isn't recognized, maybe you're building this for an iOS instead of a macOS target? In iOS `UIViewRepresentable` is the equivalent, but I left that part out, because iOS works without issue. Try removing those `UIViewRepresentable` lines too. I'll take edit them out for clarity as well. – TheNeil Jan 17 '20 at 15:19
  • 1
    Thanks @TheNeil ! Your advice helped me to get this browser page I was hoping to run, running. Now I just gotta chuck out the "Email / Browser" split and somehow make it show the full page without any extra window frills :) I really appreciate the edits and the advice. I've been hitting my head on the wall on this one for about a week now, trying to find something that would "actually work" most tutorials online are for WKWebView for iOS. – esaruoho Jan 18 '20 at 19:38
  • @esaruoho Happy to help! Glad I was able to support. This first stage of SwiftUI is a little tricky in places, but a powerful start. – TheNeil Jan 18 '20 at 19:39
  • FYI, if you do want it to be cross-platform, you can basically just slot in the UIVewRepresentable equivalent in a different, conditionally-compiled section. The code is almost identical to NSViewRepresentable, and then it’ll work on both platforms :D – TheNeil Jan 18 '20 at 19:42
  • 1
    Thanks! @TheNeil the only thing that I'm missing from this being basically "full functionality", is, that when I click on "File browser" on the browser page (to upload an image), the .app does not show me a Finder window. If I can get that running, it'll be quite wonderful :) – esaruoho Jan 18 '20 at 19:48
  • 1
    @esaruoho Best of luck! Sorry, but that’s a little outside the scope of what I need, so I’m not sure, but if I happen across a solution, I’ll update here. Maybe check that all of your app’s sandboxing settings include file access, selection, etc.? – TheNeil Jan 18 '20 at 19:55
  • 1
    No worries, you've been a big help! Just found out how to make the app-window spring up full-size. :) Just getting it to display the website was a big win for me, and you've been instrumental in that. I think I'll open up some questions on StackOverflow about the rest. Yep, I've enabled all sandboxing things as far as I can see. Have a good rest of the weekend! – esaruoho Jan 18 '20 at 19:59

3 Answers3

11

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)
}
Ahmed
  • 938
  • 7
  • 16
  • 1
    Thanks! Stupid that this workaround is needed, but at least it's something for now. – TheNeil Jan 03 '20 at 18:33
  • Wish there was an easy way to get this inside the `BrowserView` object itself, but not that I can see, because `NSViewRepresentable` doesn't use an arbitrary `View` return type. – TheNeil Jan 03 '20 at 18:35
  • 1
    Noteworthy that the issue usually occurs when these two conditions are met: - The web view isn't the first view vertically - The top container view size is calculated based on the sub views layout and needed size At least we have a work around till the next refresher for SwiftUI :) – Ahmed Jan 04 '20 at 11:56
  • Hi, where is this snippet put into in order for it to work, please? – esaruoho Jan 16 '20 at 19:00
  • @esaruoho, replace `BrowserView().tabItem { Text("Browser") }` with your WKWebView NSViewRepresentable instance – Ahmed Jan 23 '20 at 20:20
3

webView.edgesIgnoringSafeArea(.all) seems to work around this problem.

marcprux
  • 9,845
  • 3
  • 55
  • 72
1

The solution by @Ahmed with GeometryReader is quite elegant, yet I've found a shorter workaround.

struct BrowserView: View {
// ...
    var body: some View {
        // ...
        browser.padding(-1.0)
        // ...
    }
}

Seems that the default padding, which equals to 0, causes this issue. So I just set it to -1.

Dean
  • 537
  • 4
  • 8