内容变化动态更新TableViewCell高度

1,089 阅读3分钟

适用于以下场景:

  • TableViewCell 中嵌套WKWebview;
  • TableViewCell 中嵌套UITextview;
  • TableViewCell 中嵌套UICollectionview;
  • TableViewCell 中嵌套UITableView;

这里例子: TableViewCell中嵌套 WKWebview ,WKWebview内容加载完成之后,自动更新cell高度;

要实现上面👆🏻效果,我们通常有2种实现方式:

UITableview

  1. 做好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
    }
}
  1. 上面代码中👆🏻,监听到contentSize变化后,需要主线程异步更新高度约束,这个细节很重要,不然有些场景下会发现没有达到预期效果,更新高度约束没生效;
  2. 在外面,我们收到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

  1. 如果觉得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,其他的几种情况(UITextviewUICollectionviewUITableView)是一样的实现思路。
  • 这个实现思路本身并不难,笔者当时在实现这个的时候,更新高度效果一直没出来,最后尝试了很多次之后,发现是需要异步回调中刷新高度;