SwiftUI WKWebView内容高度问题

15

已经有一周了,我一直困扰于这个问题,在其中我使用自定义的WKWebView与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)
    }
}

它的呈现方式如下

在这里输入图片描述

这里的问题是webview缺少高度。 除非我添加硬编码的frame值,否则它不会出现在我的视图中,其中我的内容被截断。

Webview()
   .frame(height:300)

我几乎遇到了类似的问题,但是那些解决方案并没有帮助到我 :/

2个回答

45

在SwiftUI中,ScrollView需要预先知道内容的大小,而UIWebView内部的UIScrollView则试图从父视图中获取大小...这很容易让人困惑。

因此,可能的解决方法是将Web视图中确定的大小传递到SwiftUI世界中,以便不使用硬编码,并且ScrollView的行为就像具有平坦内容一样。

首先是结果的演示,我理解并模拟了...

enter image description here

这是演示的完整模块代码。已在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()
    }
}

1
谢谢你,@Asperi。嗯,它的工作原理就像我硬编码高度值一样,内容被截断了,就像这样https://imgur.com/IGFRf7y,视图没有适应我的内容高度(这是主要问题)。 - Mahi008
这太棒了!只是需要注意,如果您正在使用多个Webview实例,则此方法在LazyVStack中不起作用。您需要改用VStack,否则随着向下滚动,Webview的高度会越来越大。前几个大小正确,但在大约5个之后,它们开始呈指数增长。奇怪。 - Brett
谢谢,这个方法甚至可以处理来自JSON的文本,只需要绑定一个HTML字符串并将loadHTMLString...部分移动到updateUiView即可。 - Skovie
1
正如其他人所提到的 - 我爬了大量所谓的“解决方案”来解决这个问题。 完全完美 - 我还学到了一些关于元标记和其他东西的知识。非常感谢!!!(在我的情况下:iOS 15.4和Xcode 13.3.1) - NeoLeon
你可以在didFinish方法中使用webview的scrollview的contentSize属性,而不是评估JS。`func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { self.parent.dynamicHeight = webView.scrollView.contentSize.height }` - Alex Scanlan
显示剩余7条评论

3
如果我简单地将您的Webview放在VStack中,它将按预期大小进行调整。这里并没有实际问题。
尽管您没有说明,但Webview未占据任何空间的最可能原因是您将这些项目放置在ScrollView中。由于WKWebView基本上(尽管不是一个子类)是UIScrollView,当包含在另一个滚动视图中时,系统不知道如何调整其大小。
例如:
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)
        }
    }
}

嵌套滚动视图并不适合您所展示的布局。当 WKWebView 滚动时,您无疑希望顶部的图像和文本被滚动隐藏。

使用 UIKit 或 SwiftUI 的技巧将是相似的。但我目前不建议使用 SwiftUI 进行此操作。

  1. WKWebView 放置在容器中(最有可能是 UIViewController.view
  2. 将标题内容(图像加文本,可以放在 UIHostingContainer 中)作为同级元素放置在 WebView 上方。
  3. webView.scrollView.contentInset.top 设置为第二步中内容的大小
  4. 实现 UIScrollViewDelegate.scrollViewDidScroll()
  5. scrollViewDidScroll 中修改您的内容位置以匹配 webview 的 contentOffset

以下是一个可能的实现:

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。注意:我使用了常量50而不是计算大小。但是可以使用GeometryReader获取实际大小。

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,你的实现效果很好(大部分情况下),尽管我对图像+文本有点困惑。因为当我在webview上方/之前插入Image()时,它只是静止不动! - Mahi008
如果你打算继续使用SwiftUI,我建议在你的Image上使用.overlay()alignment: .top.topLeading),就像我在那里使用Text一样。只要你应用了偏移量,它应该能够正常工作。 - arsenius
谢谢@arsenius抽出时间来帮忙,但我想我会坚持使用Asperi的实现来保持简单! - Mahi008
@Mahi008 这种方法更简单,但是根据你运行的内容,通过固定webview的大小会遇到内存/速度问题。这是因为webview将不得不一次性呈现所有可能的内容,而不管它是否实际可见。对于短页面来说这没问题,但是随着页面变得更加复杂/更长,你可能会遇到问题。只要你的内容很少,那么你应该没问题。 - arsenius

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接