适用于以下场景:
- TableViewCell 中嵌套WKWebview;
- TableViewCell 中嵌套UITextview;
- TableViewCell 中嵌套UICollectionview;
- TableViewCell 中嵌套UITableView;
这里例子: TableViewCell中嵌套 WKWebview ,WKWebview内容加载完成之后,自动更新cell高度;
UITableview
- 做好TableViewCell与Webview的约束关系,并且监听Webview contentSize变化
import SnapKit
import WebKit
class TSContentTableViewCell: UITableViewCell {
/// 内容高度变化回调
var heightChangeCallback: (() -> Void)?
private var contentWebViewHeightConstraint: Constraint?
private var webViewContentSizeObservation: NSKeyValueObservation?
/// webview加载完成
private var isContentWebViewDidFinish = false
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupView()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private lazy var contentWebView: WKWebView = {
let result = WKWebView()
result.scrollView.isScrollEnabled = false
result.isUserInteractionEnabled = false
result.navigationDelegate = self
return result
}()
deinit {
webViewContentSizeObservation?.invalidate()
}
private func setupView() {
contentView.addSubview(contentWebView)
/// 这里约束的思路就是,让contentWebView撑起cell内容高度
contentWebView.snp.makeConstraints { make in
make.edges.equalToSuperview()
contentWebViewHeightConstraint = make.height.equalTo(0).constraint
}
}
}
// MARK: - Event
extension TSContentTableViewCell {
private func addObserver() {
/// 监听 webview contentSize变化
webViewContentSizeObservation = contentWebView.scrollView.observe(\.contentSize, options: [.new, .old]) { _, _ in
self.webViewContentSizeChange()
}
}
private func webViewContentSizeChange() {
/// 需要等到页面渲染完成之后再设置webview高度,避免出现第一次获取contentSize不准,导致设置webview高度约束后,
/// 无法再继续获取到真实的contentSize
guard isContentWebViewDidFinish else { return }
contentWebView.sizeToFit()
/// 这里需要异步处理 [重要!!!]
DispatchQueue.main.async { [weak self] in
guard let self else { return }
let height = self.contentWebView.scrollView.contentSize.height
/// 更新 高度约束
self.contentWebViewHeightConstraint?.update(offset: height)
/// 高度变化回调,外面刷新处理
self.heightChangeCallback?()
}
}
}
extension TSContentTableViewCell: WKNavigationDelegate {
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
isContentWebViewDidFinish = true
}
}
- 上面代码中👆🏻,监听到
contentSize变化后,需要主线程异步更新高度约束,这个细节很重要,不然有些场景下会发现没有达到预期效果,更新高度约束没生效; - 在外面,我们收到cell的
heightChangeCallback的回调后,我们需要进行刷新处理,当然这里的刷新是不要直接tableView.reloadData()这么简单粗暴,因为此时我们只想更新 cell高度,并不想刷新整个TableView列表;这时候就可以使用下面👇🏻的方式来处理,在cellForRow方法中
cell.heightChangeCallback = { [weak self] in
guard let self else { return }
UIView.performWithoutAnimation {
self.tableView.performBatchUpdates {}
}
}
这样就会去批量更新tabeleView cell的frame,而且不会触发 UITableViewDataSource 方法的调用;
UIScrollView
- 如果觉得TableView处理起来比较麻烦,可以考虑使用UIScrollView 处理,会更方便,我们只需要做好布局,然后监听WKWebview的contentSize变化,然后再更新 WKWebview的高度约束即可。这里就不详细展开
封装
- 如果在项目中有其他地方也用到类似的场景,我们可以将其封装起来,方便复用,外面在用的时候,就只需要关心
heightChangeCallback回调,然后刷新即可
import SnapKit
import UIKit
import WebKit
class WebViewContainerView: UIView {
var heightChangeCallback: (() -> Void)?
private var contentWebViewHeightConstraint: Constraint?
private var webViewContentSizeObservation: NSKeyValueObservation?
private var isContentWebViewDidFinish = false
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
webViewContentSizeObservation?.invalidate()
}
lazy var contentWebView: WKWebView = {
let result = WKWebView()
result.scrollView.isScrollEnabled = false
result.isUserInteractionEnabled = false
result.navigationDelegate = self
return result
}()
private func setupView() {
addSubview(contentWebView)
contentWebView.snp.makeConstraints { make in
make.edges.equalToSuperview()
contentWebViewHeightConstraint = make.height.equalTo(0).constraint
}
}
}
// MARK: - Event
extension WebViewContainerView {
private func addObserver() {
webViewContentSizeObservation = contentWebView.scrollView.observe(\.contentSize, options: [.new, .old]) { _, _ in
self.webViewContentSizeChange()
}
}
private func webViewContentSizeChange() {
/// 需要等到页面渲染完成之后再设置webview高度,避免出现第一次获取contentSize不准,导致设置webview高度约束后,
/// 无法再继续获取到真实的contentSize
guard isContentWebViewDidFinish else { return }
contentWebView.sizeToFit()
/// 这里需要异步处理 [重要!!!]
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
let height = self.contentWebView.scrollView.contentSize.height
self.contentWebViewHeightConstraint?.update(offset: height)
self.heightChangeCallback?()
}
}
}
extension WebViewContainerView: WKNavigationDelegate {
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
isContentWebViewDidFinish = true
}
}
使用举例,这里还以上面 TSContentTableViewCell中使用举例
private lazy var webContainerView: WebViewContainerView = {
let view = WebViewContainerView()
return view
}()
private func setupView() {
contentView.addSubview(webContainerView)
webContainerView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
webContainerView.heightChangeCallback = { [weak self] in
guard let self else { return }
self.heightChangeCallback?()
}
}
最后
- 这里列举的是
TableViewCell中嵌套WKWebview,其他的几种情况(UITextview、UICollectionview、UITableView)是一样的实现思路。 - 这个实现思路本身并不难,笔者当时在实现这个的时候,更新高度效果一直没出来,最后尝试了很多次之后,发现是需要异步回调中刷新高度;