14

It's been a week I'm stuck with this issue where I have my custom WKWebView with UIViewRepresentable

struct Webview : UIViewRepresentable {
    var webview: WKWebView?

    init() {
        self.webview = WKWebView()
    }

    class Coordinator: NSObject, WKNavigationDelegate {
        var parent: Webview

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

        func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
            print("Loading finished -- Delegate")
            webView.evaluateJavaScript("document.documentElement.scrollHeight", completionHandler: { (height, error) in
                print(height)
                webView.bounds.size.height = height as! CGFloat
            })
        }

    }

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

    func makeUIView(context: Context) -> WKWebView  {
        return webview!
    }

    func updateUIView(_ uiView: WKWebView, context: Context) {
        uiView.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)"
        uiView.loadHTMLString(htmlString, baseURL:  nil)
    }
}

and it's presented like this

enter image description here

and the issue here is the webview is missing height. It's not appearing in my view unless I add a hard coded frame value where my content gets cut-off.

Webview()
   .frame(height:300)

I almost came across similar questions but it didn't help :/

Mahi008
  • 373
  • 2
  • 3
  • 22

2 Answers2

44

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 ...

enter image description here

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()
    }
}
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • 1
    Thanks you @Asperi Well it's working like If have hardcoded the height value, the content is cut-off like so https://imgur.com/IGFRf7y, view is not adapting to my content height (which in case the main issue here) – Mahi008 Jan 17 '20 at 16:08
  • No sure I understood the problem... `UIViewRepresentable` consumes available space, so if it is less than content of `WebView` then yes, it will be cut, but `WebView` is scrollable, so cut content is available by scrolling. So? – Asperi Jan 17 '20 at 16:18
  • I already have a Scrollview which contains ( Image + Webview : scrollview disabled to avoid 2 scroll bars). So when the webview loads the height should be = it's content length. Basically acts like a UILabel which expands automatically depending on its content. – Mahi008 Jan 17 '20 at 16:51
  • BTW I'm sorry i was wrong, it's not working from the beginning (even your answer) because I forgot to remove hard coded frame value in my view `.frame(height:300)` – Mahi008 Jan 17 '20 at 17:07
  • @Mahi008, please see updated solution for the scenario as you described above. – Asperi Jan 24 '20 at 07:00
  • This works great, even if the WKWebView is already inside another ScrollView. – iMaddin Jul 10 '20 at 10:31
  • @Asperi Thanks, but I use Down (markdown parser) and it's work with WKWebView so I have the same problem but I need to implant it in a UIViewController and an override ViewDidLoad() and I don't arrive input the height in the dynamicHeight variable. Can you help me? – Mathieu Cloart Jul 24 '20 at 00:10
  • Thank you, you're a life saver I had to search for over 10 articles and videos to finally find something that works with SwiftUI 5. – Merunas Grincalaitis Aug 13 '20 at 19:59
  • This is great!!! Just a note that this does not work in a LazyVStack if you are using multiple instances of Webview as I am. You will need to use a VStack instead, otherwise the Webview heights gets increasingly larger as you scroll down. The first couple are sized correctly, but after around 5 of them they start to increase expodentially. Weird. – Brett May 14 '21 at 01:05
  • Thanks , this works even with text from json , just had so bind a html string and move the loadHTMLString.... part to updateUiView – Skovie Sep 14 '21 at 21:19
  • 1
    As other mentioned - I crawled a ton of so called 'solutions' for this problem. Works absolutely perfect - and I learned something about meta tags and stuff . THANK YOU VERY MUCH!!! (In my case: iOS 15.4 and Xcode 13.3.1) – NeoLeon May 05 '22 at 13:22
  • You can use the contentSize of the webview's scrollview in didFinish instead of evaluating JS `func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { self.parent.dynamicHeight = webView.scrollView.contentSize.height }` – Alex Scanlan Oct 07 '22 at 11:48
3

If I simply place your Webview in a VStack it sizes as expected. Theres' no actual problem there.

Although you haven't stated so, the most likely reason for your Webview not taking up any space is that you have placed these items in a ScrollView. Since WKWebView is basically (though not a subclass of) a UIScrollView the system does not know how to size it when contained within another scrolling view.

For example:

struct ContentView: View {
    var body: some View {
        NavigationView {
            ScrollView { // THIS LINE means that...
                VStack(spacing: 0.0) {
                    Color.red.aspectRatio(1.0, contentMode: .fill)
                    Text("Lorem ipsum dolor sit amet").fontWeight(.bold)
                    Webview()
                        .frame(height: 300) // ...THIS is necessary 
                }
            }
        .navigationBarItems(leading: Text("Item"))
            .navigationBarTitle("", displayMode: .inline)
        }
    }
}

Nested scrollviews is not what you want for the layout you've shown anyway. You no doubt want that image and text at the top to scroll out of the way when the WKWebView scrolls.

The technique you need to use with either UIKit or SwiftUI is going to be similar. I don't really recommend doing this with SwiftUI at this point.

  1. Place the WKWebView in a container (most likely UIViewController.view)
  2. Place your header content (image plus text, which could be in a UIHostingContainer) in that view as a sibling above the webview.
  3. Set webView.scrollView.contentInset.top to the size of your content from #2
  4. Implement UIScrollViewDelegate.scrollViewDidScroll()
  5. In scrollViewDidScroll modify the position of your content to match the webview's contentOffset.

Here is one possible implementation:

WebView:

import SwiftUI
import WebKit

struct WebView : UIViewRepresentable {

    @Binding var offset: CGPoint
    var contentInset: UIEdgeInsets = .zero

    class Coordinator: NSObject, UIScrollViewDelegate {
        var parent: WebView

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

        func scrollViewDidScroll(_ scrollView: UIScrollView) {
            var offset = scrollView.contentOffset
            offset.y += self.parent.contentInset.top
            self.parent.offset = offset
        }
    }

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

    func makeUIView(context: Context) -> WKWebView {
        let webview = WKWebView()
        webview.scrollView.delegate = context.coordinator
        // continue setting up webview content
        return webview
    }

    func updateUIView(_ uiView: WKWebView, context: Context) {
        if uiView.scrollView.contentInset != self.contentInset {
            uiView.scrollView.contentInset = self.contentInset
        }
    }

}

ContentView. Note: I've used 50 as a constant instead of calculating the size. It is possible to get the actual size using GeometryReader though.

struct ContentView: View {
    @State var offset: CGPoint = .zero
    var body: some View {
        NavigationView {
            WebView(offset: self.$offset, contentInset: UIEdgeInsets(top: 50, left: 0, bottom: 0, right: 0))
            .overlay(
                Text("Hello World!")
                    .frame(height: 50)
                    .offset(y: -self.offset.y)
                , alignment: .topLeading)
                .edgesIgnoringSafeArea(.all)
        }
        .navigationBarItems(leading: Text("Item"))
            .navigationBarTitle("", displayMode: .inline)
    }
}
arsenius
  • 12,090
  • 7
  • 58
  • 76
  • Thank you @arsenius, your implementation works well (for the most part) and even though I'm little confused on the Image + Text. Because when I insert Image() on top of / before webview, it just sits there without scrolling ! – Mahi008 Jan 22 '20 at 13:23
  • If you're going to stick with SwiftUI I would recommend using `.overlay()` wich `alignment:` `.top` or `.topLeading)` for your `Image`, as I did with `Text` there. As long as you then apply the offset it should work the same. – arsenius Jan 22 '20 at 23:21
  • Thank you @arsenius for your time, but I think i will stick with Asperi's implementation to keep it simple ! – Mahi008 Jan 25 '20 at 09:39
  • @Mahi008 It is simpler, but depending on the content you will run into memory/speed issues by fixing the size of the webview. This is because the webview will have to render all possible content at once, regardless of whether it's actually visible. This is fine for short pages, but as pages become more complex/longer you may run into issues. As long as you have minimal content though, you should be ok. – arsenius Jan 26 '20 at 10:55