Search code examples
iosswiftxcodexib

How to auto size UITableViewCell with underlying WKWebKit, so it respects the content of the web view?


In my iOS app I want to display several items that contain some HTML. For this I've build a NewsListViewController that contains a UITableView. For that UITableView (outlet: newsListTableView) I created a custom UITableViewCell called NewsTableViewCell that will be load into it via delegates. The NewsTableViewCell holds a WKWebKit control that will display my HTML. This is my code:

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

NewsTableViewCell.xib preview

My WKWebKit (outlet: newsContentPreviewWebView) is lying inside of an UIView (outlet: newsContentViewContainer) with proper constraints to grow with the UIView. My UIView comes with proper constraints to grow with the whole cell, too.

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 {
}

This is the result:

App preview without auto sizing

Now, I want the cells to be as small as possible, but still displaying the full WKWebKit content.

When I remove ...

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat
{
    return 300.0
}

... from NewsListViewController.swift the cell height will only respect the labels above the WKWebKit control:

App preview showing failed auto sizing of the cell

I think this is happening, because my WKWebKit's content is not loaded when the app sizes the cells.

I tried to overcome this problem by listening to the WKWebKit's >> did finish navigation << delegate call in my NewsTableViewCell.swift and resizing the parent view and whole cell height like so:

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)
                })
            }
        })
    }
}

This is the result:

App preview showing overlapping of cells when trying to set the size, manually

The cells are overlapping which indicates that I'm trying to solve this in the wrong way.

What is the correct way to auto size the custom table cells to fit all its content including the content of the WKWebKit?


Solution

  • The problem is you never give to your cells a height. Setting the frame by hand

    self.frame = CGRect(x: tableCellFrame.minX, y: tableCellFrame.minY, width: tableCellFrame.width, height: tableCellFrame.height + h)

    is bad. You should always use the dedicated delegate method or the estimated row height technique (Using Auto Layout in UITableView for dynamic cell layouts & variable row heights).

    The problem is sticky : you need to wait for the webview to load to calculate its height. But you need its height when heightForCell is called so before the content of the webview is loaded.

    I see 5 solutions :

    • You transform your tableview into a giant webview containing all the news. And you inject the meta data by hand into the html
    • You parse the html and extract the info you need. This is possible only if all the news html have the same structure. And this is pretty hard too (RegEx match open tags except XHTML self-contained tags).
    • You proceed as you did. But when completionHandler is called, you call a delegate (your viewcontroller for instance) that will reload the corresponding cell and giving it its right height just calculated. This solution is simple but before each loading, you need to give at your cell a wrong height. So, you could define a loading state in your cell but you will always have a little glitch when a cell is about to appear.
    • You load the content of all of your cells and calculate their corresponding height on a background thread before displaying the tableview. You could use an Operation for each of your cells that will wait for the html of the cell to load and evaluate the given javascript. When all the operations are done, you reload your tableview
    • Change your design : display only a list of news and load the news content inside a separate view controller when a cell is selected