如何使用底层 WKWebKit 自动调整 UITableViewCell 的大小,使其尊重 Web 视图的内容?
How to auto size UITableViewCell with underlying WKWebKit, so it respects the content of the web view?
在我的 iOS 应用程序中,我想显示包含一些 HTML 的几个项目。为此,我构建了一个包含 UITableView 的 NewsListViewController。为此 UITableView(出口:newsListTableView)我创建了一个名为 NewsTableViewCell 的自定义 UITableViewCell 将通过委托加载到其中。 NewsTableViewCell 包含一个 WKWebKit 控件,它将显示我的 HTML。这是我的代码:
NewsListViewController.swift
import UIKit
class NewsListViewController: UIViewController {
@IBOutlet weak var newsListTableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
self.newsListTableView.delegate = self
self.newsListTableView.dataSource = self
self.newsListTableView.register(UINib(nibName: "NewsTableViewCell", bundle: nil), forCellReuseIdentifier: "cell")
}
}
extension NewsListViewController: UITableViewDelegate {
}
extension NewsListViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 3
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat
{
return 300.0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! NewsTableViewCell
cell.titleText = "My Title"
cell.dateTimeText = "01.01.1998"
cell.contentHtml = "<html><head><meta name=\"viewport\" content=\"initial-scale=1.0\" /><style>html, body { margin: 0; padding: 0; font-size: 15px; }</style></head><body><b>Hello News</b><br />Hello News<br />Hello News<br />Hello News</body></html>"
return cell
}
}
NewsTableViewCell.xib
我的WKWebKit(出口:newsContentPreviewWebView)位于UIView(出口: newsContentViewContainer) 具有适当的约束以随 UIView 一起增长。我的 UIView 也带有适当的约束,可以随着整个单元格的增长而增长。
NewsTableViewCell.swift
import UIKit
import WebKit
@IBDesignable class NewsTableViewCell: UITableViewCell {
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var dateTimeLabel: UILabel!
@IBOutlet weak var newsContentViewContainer: UIView!
@IBOutlet weak var newsContentPreviewWebView: WKWebView!
@IBInspectable var titleText: String? {
get {
return self.titleLabel.text
}
set(value) {
self.titleLabel.text = value
}
}
@IBInspectable var dateTimeText: String? {
get {
return self.dateTimeLabel.text
}
set(value) {
self.dateTimeLabel.text = value
}
}
@IBInspectable var contentHtml: String? {
get {
return nil
}
set(value) {
self.newsContentPreviewWebView.loadHTMLString(value ?? "", baseURL: nil)
}
}
override func awakeFromNib() {
super.awakeFromNib()
self.newsContentPreviewWebView.navigationDelegate = self
}
}
extension NewsTableViewCell: WKNavigationDelegate {
}
这是结果:
现在,我希望单元格尽可能小,但仍显示完整的 WKWebKit 内容。
当我删除...
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat
{
return 300.0
}
... 来自 NewsListViewController.swift 单元格高度将仅尊重 WKWebKit 控件上方的标签:
我认为会发生这种情况,因为当应用调整单元格大小时我的 WKWebKit 的内容没有加载。
我试图通过听取 WKWebKit 的 >> 完成导航 << 我的 NewsTableViewCell.swift[ 中的委托调用来解决这个问题=78=] 并像这样调整父视图和整个单元格的高度:
extension NewsTableViewCell: WKNavigationDelegate {
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
webView.evaluateJavaScript("document.readyState", completionHandler: { (complete, error) in
if complete != nil {
webView.evaluateJavaScript("document.body.scrollHeight", completionHandler: { (height, error) in
let h: CGFloat = height as! CGFloat
let newsContentFrame: CGRect = self.newsContentViewContainer.frame
self.newsContentViewContainer.frame = CGRect(x: newsContentFrame.minX, y: newsContentFrame.minY, width: newsContentFrame.width, height: h)
let tableCellFrame: CGRect = self.frame
self.frame = CGRect(x: tableCellFrame.minX, y: tableCellFrame.minY, width: tableCellFrame.width, height: tableCellFrame.height + h)
})
}
})
}
}
这是结果:
单元格重叠,这表明我试图以错误的方式解决这个问题。
自动调整自定义 table 单元格大小以适应其所有内容(包括 WKWebKit 内容的正确方法是什么?
问题是你从来没有给你的单元格一个高度。手动设置框架
self.frame = CGRect(x: tableCellFrame.minX, y: tableCellFrame.minY,
width: tableCellFrame.width, height: tableCellFrame.height + h)
不好。您应该始终使用专用委托方法或估计行高技术 (Using Auto Layout in UITableView for dynamic cell layouts & variable row heights)。
问题很棘手:您需要等待 webview 加载才能计算其高度。但是在加载 webview 的内容之前调用 heightForCell
时你需要它的高度。
我看到 5 个解决方案:
- 您将 tableview 转换为包含所有新闻的巨大 webview。然后您手动将元数据注入 html
- 您解析 html 并提取您需要的信息。这只有在所有新闻 html 具有相同结构时才有可能。这也很难(RegEx match open tags except XHTML self-contained tags)。
- 您照原样进行。但是当
completionHandler
被调用时,您调用一个委托(例如您的 viewcontroller ),它将重新加载相应的单元格并为其提供刚刚计算的正确高度。这个解决方案很简单,但在每次加载之前,你需要在你的单元格中给出一个错误的高度。因此,您可以在单元格中定义一个加载状态,但是当单元格即将出现时,您总会遇到一些小故障。
- 在显示表格视图之前,加载所有单元格的内容并在后台线程上计算它们相应的高度。您可以为每个将等待单元格的 html 加载和评估给定 javascript 的单元格使用
Operation
。完成所有操作后,重新加载 tableview
- 更改您的设计:仅显示新闻列表并在选择单元格时将新闻内容加载到单独的视图控制器中
我有这个,在 list/table 视图中添加一点延迟以正确计算 contentSize 的高度似乎很重要。
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
webView.evaluateJavaScript("document.readyState", completionHandler: { (ready, error) in
if ready != nil {
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(500)) {
self.parent.contentHeight = webView.scrollView.contentSize.height
print("WKWebView height: \(webView.scrollView.contentSize.height)")
}
}
})
parent.didFinish(navigation)
}
有时,"didFinish" 被调用,但内容仍在像图像一样加载。如果 html 包含图像或其他资源,那么即使在 didFinish 调用后也需要很长时间才能呈现。即使您使用 "evaluateJavascript" 方法和 "document.readyState", "document.documentElement.scrollHeight" in "didFinish" .
所以,问题是您仍然没有获得实际高度,这导致单元格的高度较小而不是完整高度。
didFinish 之后的延迟将起作用,但不是正确的解决方案。
所以,我所做的只是创建 WKWebview 并将 html 加载到其中并等待“didFinish”委托。一旦调用了“didFinish”,我便将 WKwebview 添加到单元格中。它对我来说完全没问题。
检查我的解决方案:
A) WKWebView
var arrData:[Int] = [Int]() //Datasource array: handle according to your need
private var webViewHeight:CGFloat = 0 // WKWEbview height
lazy var quesWebView:WKWebView = {
let configuration = WKWebViewConfiguration()
return WKWebView(frame: CGRect.zero, configuration: configuration)
}()
B) 创建 HTML 数据并将其加载到 WKWebview
override func viewDidLoad() {
super.viewDidLoad()
tbQuestion.estimatedRowHeight = 50
tbQuestion.rowHeight = UITableView.automaticDimension
tbQuestion.register(UINib(nibName: String(describing:TestQuestionDescriptionCell.self), bundle: nil), forCellReuseIdentifier: String(describing:TestQuestionDescriptionCell.self))//describing:TestQuestionDescriptionCell is the tableviewCell with XIB.
quesWebView.navigationDelegate = self
quesWebView.scrollView.isScrollEnabled = false
quesWebView.scrollView.bounces = false
quesWebView.isUserInteractionEnabled = false
quesWebView.contentMode = .scaleToFill
let htmlStr = "yours_html"
let strTemplateHTML = "<html><head>\(TestConstants.kHTMLViewPort)<style>img{max-width:\(UIScreen.main.bounds.size.width - 20);height:auto !important;width:auto !important;};</style></head><body style='margin:0; padding:0;'>\(htmlStr)</body></html>"
// img is the CSS to handle images according to screen width or screen size.
let data = strTemplateHTML.getHTMLData()
if let d = data { // data is the HTML data
self.quesWebView.load(d, mimeType: "text/html", characterEncodingName:"UTF-8", baseURL: Bundle.main.bundleURL)
// You can also load HTML string with loadWithStirng method. We use Encoding to show text it in differnt languages also.
}
}
测试常量文件是:
// 视口处理 WKWebview 上的内容渲染,因为内容需要缩放、缩放等。
struct TestConstants {
static var kHTMLViewPort:String = "<meta name='viewport' content='width=device-width, shrink-to-fit=YES' initial-scale='1.0' maximum-scale='1.0' minimum-scale='1.0' user-scalable='no'>"
}
注意:我们也可以使用 scrpit 添加视口。
用于将 HTML 字符串转换为 html 数据的扩展名:
extension String {
func getHTMLData() -> Data? {
let htmlData = NSString(string: self).data(using: String.Encoding.unicode.rawValue)
return htmlData
}
}
c) "didFinish" 代表:
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
webView.evaluateJavaScript("document.readyState", completionHandler: { (complete, error) in
if complete != nil {
webView.evaluateJavaScript("document.documentElement.scrollHeight", completionHandler: { (height, error) in
guard let heightT = height as? CGFloat else { return }
DispatchQueue.main.async {
self.webViewHeight = heightT
webView.invalidateIntrinsicContentSize()
arrData.append(1) // Add an item to datasource array
self.tbQuestion.reloadData()
}
})
}
})
}
D) 表视图委托:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return arrData.count
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return webViewHeight
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(withIdentifier: TestQuestionDescriptionCell.className, for: indexPath) as? TestQuestionDescriptionCell {
cell.addWebView(webView:quesWebView)
cell.selectionStyle = .none
return cell
}
}
E)自定义表格视图class
import UIKit
import WebKit
class TestQuestionDescriptionCell: UITableViewCell {
private var webView: WKWebView?
@IBOutlet weak var containerView: UIView!
var isWebViewAdded:Bool = false
override func awakeFromNib() {
super.awakeFromNib()
}
func addWebView(webView:WKWebView) {
if self.webView == nil {
self.webView = webView
if let quesWebView = self.webView, !isWebViewAdded {
isWebViewAdded = true
contentView.addSubview(quesWebView)
quesWebView.translatesAutoresizingMaskIntoConstraints = false
quesWebView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 0).isActive = true
quesWebView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: 0).isActive = true
quesWebView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 0).isActive = true
quesWebView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: 0).isActive = true
}
}
}
}
E) 重要提示:不要忘记在需要导入的地方导入 Webkit。
在我的 iOS 应用程序中,我想显示包含一些 HTML 的几个项目。为此,我构建了一个包含 UITableView 的 NewsListViewController。为此 UITableView(出口:newsListTableView)我创建了一个名为 NewsTableViewCell 的自定义 UITableViewCell 将通过委托加载到其中。 NewsTableViewCell 包含一个 WKWebKit 控件,它将显示我的 HTML。这是我的代码:
NewsListViewController.swift
import UIKit
class NewsListViewController: UIViewController {
@IBOutlet weak var newsListTableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
self.newsListTableView.delegate = self
self.newsListTableView.dataSource = self
self.newsListTableView.register(UINib(nibName: "NewsTableViewCell", bundle: nil), forCellReuseIdentifier: "cell")
}
}
extension NewsListViewController: UITableViewDelegate {
}
extension NewsListViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 3
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat
{
return 300.0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! NewsTableViewCell
cell.titleText = "My Title"
cell.dateTimeText = "01.01.1998"
cell.contentHtml = "<html><head><meta name=\"viewport\" content=\"initial-scale=1.0\" /><style>html, body { margin: 0; padding: 0; font-size: 15px; }</style></head><body><b>Hello News</b><br />Hello News<br />Hello News<br />Hello News</body></html>"
return cell
}
}
NewsTableViewCell.xib
我的WKWebKit(出口:newsContentPreviewWebView)位于UIView(出口: newsContentViewContainer) 具有适当的约束以随 UIView 一起增长。我的 UIView 也带有适当的约束,可以随着整个单元格的增长而增长。
NewsTableViewCell.swift
import UIKit
import WebKit
@IBDesignable class NewsTableViewCell: UITableViewCell {
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var dateTimeLabel: UILabel!
@IBOutlet weak var newsContentViewContainer: UIView!
@IBOutlet weak var newsContentPreviewWebView: WKWebView!
@IBInspectable var titleText: String? {
get {
return self.titleLabel.text
}
set(value) {
self.titleLabel.text = value
}
}
@IBInspectable var dateTimeText: String? {
get {
return self.dateTimeLabel.text
}
set(value) {
self.dateTimeLabel.text = value
}
}
@IBInspectable var contentHtml: String? {
get {
return nil
}
set(value) {
self.newsContentPreviewWebView.loadHTMLString(value ?? "", baseURL: nil)
}
}
override func awakeFromNib() {
super.awakeFromNib()
self.newsContentPreviewWebView.navigationDelegate = self
}
}
extension NewsTableViewCell: WKNavigationDelegate {
}
这是结果:
现在,我希望单元格尽可能小,但仍显示完整的 WKWebKit 内容。
当我删除...
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat
{
return 300.0
}
... 来自 NewsListViewController.swift 单元格高度将仅尊重 WKWebKit 控件上方的标签:
我认为会发生这种情况,因为当应用调整单元格大小时我的 WKWebKit 的内容没有加载。
我试图通过听取 WKWebKit 的 >> 完成导航 << 我的 NewsTableViewCell.swift[ 中的委托调用来解决这个问题=78=] 并像这样调整父视图和整个单元格的高度:
extension NewsTableViewCell: WKNavigationDelegate {
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
webView.evaluateJavaScript("document.readyState", completionHandler: { (complete, error) in
if complete != nil {
webView.evaluateJavaScript("document.body.scrollHeight", completionHandler: { (height, error) in
let h: CGFloat = height as! CGFloat
let newsContentFrame: CGRect = self.newsContentViewContainer.frame
self.newsContentViewContainer.frame = CGRect(x: newsContentFrame.minX, y: newsContentFrame.minY, width: newsContentFrame.width, height: h)
let tableCellFrame: CGRect = self.frame
self.frame = CGRect(x: tableCellFrame.minX, y: tableCellFrame.minY, width: tableCellFrame.width, height: tableCellFrame.height + h)
})
}
})
}
}
这是结果:
单元格重叠,这表明我试图以错误的方式解决这个问题。
自动调整自定义 table 单元格大小以适应其所有内容(包括 WKWebKit 内容的正确方法是什么?
问题是你从来没有给你的单元格一个高度。手动设置框架
self.frame = CGRect(x: tableCellFrame.minX, y: tableCellFrame.minY, width: tableCellFrame.width, height: tableCellFrame.height + h)
不好。您应该始终使用专用委托方法或估计行高技术 (Using Auto Layout in UITableView for dynamic cell layouts & variable row heights)。
问题很棘手:您需要等待 webview 加载才能计算其高度。但是在加载 webview 的内容之前调用 heightForCell
时你需要它的高度。
我看到 5 个解决方案:
- 您将 tableview 转换为包含所有新闻的巨大 webview。然后您手动将元数据注入 html
- 您解析 html 并提取您需要的信息。这只有在所有新闻 html 具有相同结构时才有可能。这也很难(RegEx match open tags except XHTML self-contained tags)。
- 您照原样进行。但是当
completionHandler
被调用时,您调用一个委托(例如您的 viewcontroller ),它将重新加载相应的单元格并为其提供刚刚计算的正确高度。这个解决方案很简单,但在每次加载之前,你需要在你的单元格中给出一个错误的高度。因此,您可以在单元格中定义一个加载状态,但是当单元格即将出现时,您总会遇到一些小故障。 - 在显示表格视图之前,加载所有单元格的内容并在后台线程上计算它们相应的高度。您可以为每个将等待单元格的 html 加载和评估给定 javascript 的单元格使用
Operation
。完成所有操作后,重新加载 tableview - 更改您的设计:仅显示新闻列表并在选择单元格时将新闻内容加载到单独的视图控制器中
我有这个,在 list/table 视图中添加一点延迟以正确计算 contentSize 的高度似乎很重要。
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
webView.evaluateJavaScript("document.readyState", completionHandler: { (ready, error) in
if ready != nil {
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(500)) {
self.parent.contentHeight = webView.scrollView.contentSize.height
print("WKWebView height: \(webView.scrollView.contentSize.height)")
}
}
})
parent.didFinish(navigation)
}
有时,"didFinish" 被调用,但内容仍在像图像一样加载。如果 html 包含图像或其他资源,那么即使在 didFinish 调用后也需要很长时间才能呈现。即使您使用 "evaluateJavascript" 方法和 "document.readyState", "document.documentElement.scrollHeight" in "didFinish" .
所以,问题是您仍然没有获得实际高度,这导致单元格的高度较小而不是完整高度。
didFinish 之后的延迟将起作用,但不是正确的解决方案。 所以,我所做的只是创建 WKWebview 并将 html 加载到其中并等待“didFinish”委托。一旦调用了“didFinish”,我便将 WKwebview 添加到单元格中。它对我来说完全没问题。
检查我的解决方案:
A) WKWebView
var arrData:[Int] = [Int]() //Datasource array: handle according to your need
private var webViewHeight:CGFloat = 0 // WKWEbview height
lazy var quesWebView:WKWebView = {
let configuration = WKWebViewConfiguration()
return WKWebView(frame: CGRect.zero, configuration: configuration)
}()
B) 创建 HTML 数据并将其加载到 WKWebview
override func viewDidLoad() {
super.viewDidLoad()
tbQuestion.estimatedRowHeight = 50
tbQuestion.rowHeight = UITableView.automaticDimension
tbQuestion.register(UINib(nibName: String(describing:TestQuestionDescriptionCell.self), bundle: nil), forCellReuseIdentifier: String(describing:TestQuestionDescriptionCell.self))//describing:TestQuestionDescriptionCell is the tableviewCell with XIB.
quesWebView.navigationDelegate = self
quesWebView.scrollView.isScrollEnabled = false
quesWebView.scrollView.bounces = false
quesWebView.isUserInteractionEnabled = false
quesWebView.contentMode = .scaleToFill
let htmlStr = "yours_html"
let strTemplateHTML = "<html><head>\(TestConstants.kHTMLViewPort)<style>img{max-width:\(UIScreen.main.bounds.size.width - 20);height:auto !important;width:auto !important;};</style></head><body style='margin:0; padding:0;'>\(htmlStr)</body></html>"
// img is the CSS to handle images according to screen width or screen size.
let data = strTemplateHTML.getHTMLData()
if let d = data { // data is the HTML data
self.quesWebView.load(d, mimeType: "text/html", characterEncodingName:"UTF-8", baseURL: Bundle.main.bundleURL)
// You can also load HTML string with loadWithStirng method. We use Encoding to show text it in differnt languages also.
}
}
测试常量文件是: // 视口处理 WKWebview 上的内容渲染,因为内容需要缩放、缩放等。
struct TestConstants {
static var kHTMLViewPort:String = "<meta name='viewport' content='width=device-width, shrink-to-fit=YES' initial-scale='1.0' maximum-scale='1.0' minimum-scale='1.0' user-scalable='no'>"
}
注意:我们也可以使用 scrpit 添加视口。
用于将 HTML 字符串转换为 html 数据的扩展名:
extension String {
func getHTMLData() -> Data? {
let htmlData = NSString(string: self).data(using: String.Encoding.unicode.rawValue)
return htmlData
}
}
c) "didFinish" 代表:
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
webView.evaluateJavaScript("document.readyState", completionHandler: { (complete, error) in
if complete != nil {
webView.evaluateJavaScript("document.documentElement.scrollHeight", completionHandler: { (height, error) in
guard let heightT = height as? CGFloat else { return }
DispatchQueue.main.async {
self.webViewHeight = heightT
webView.invalidateIntrinsicContentSize()
arrData.append(1) // Add an item to datasource array
self.tbQuestion.reloadData()
}
})
}
})
}
D) 表视图委托:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return arrData.count
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return webViewHeight
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(withIdentifier: TestQuestionDescriptionCell.className, for: indexPath) as? TestQuestionDescriptionCell {
cell.addWebView(webView:quesWebView)
cell.selectionStyle = .none
return cell
}
}
E)自定义表格视图class
import UIKit
import WebKit
class TestQuestionDescriptionCell: UITableViewCell {
private var webView: WKWebView?
@IBOutlet weak var containerView: UIView!
var isWebViewAdded:Bool = false
override func awakeFromNib() {
super.awakeFromNib()
}
func addWebView(webView:WKWebView) {
if self.webView == nil {
self.webView = webView
if let quesWebView = self.webView, !isWebViewAdded {
isWebViewAdded = true
contentView.addSubview(quesWebView)
quesWebView.translatesAutoresizingMaskIntoConstraints = false
quesWebView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 0).isActive = true
quesWebView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: 0).isActive = true
quesWebView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 0).isActive = true
quesWebView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: 0).isActive = true
}
}
}
}
E) 重要提示:不要忘记在需要导入的地方导入 Webkit。