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()
}
}
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.
- Place the
WKWebView
in a container (most likelyUIViewController.view
) - Place your header content (image plus text, which could be in a
UIHostingContainer
) in that view as a sibling above the webview. - Set
webView.scrollView.contentInset.top
to the size of your content from #2 - Implement
UIScrollViewDelegate.scrollViewDidScroll()
- In
scrollViewDidScroll
modify the position of your content to match the webview'scontentOffset
.
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)
}
}