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
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:
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:
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:
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?
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 :
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.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 tableviewSometimes, "didFinish" called and the content is still loading like images. If the html contains images or other resources, then it will take long time to render even after the didFinish called. This remains same even you use "evaluateJavascript" method with "document.readyState", "document.documentElement.scrollHeight" in "didFinish".
So, the thing is you are still not getting the actual height which results in small height of the cell instead of the full height.
Delay after the didFinish will work but not a proper solution. So, what I have done, I just create the WKWebview and load the html into it and wait for the "didFinish" delegate. Once, "didFinish" is called, then I add the WKwebview to the cell. It works completely fine with me.
Check my solution to do that:
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) Creating HTML data and loading it onto 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.
}
}
Test Constant file is: // Viewport handles the content rendering on WKWebview as content need to scaled, zoomed, etc.
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'>"
}
NOTE: We can also use scrpit to add the viewport.
Extension Used to convert HTML string to html data:
extension String {
func getHTMLData() -> Data? {
let htmlData = NSString(string: self).data(using: String.Encoding.unicode.rawValue)
return htmlData
}
}
c) "didFinish" Delegate:
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) Tableview delegates:
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)Custom TableView 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) IMPORTANT: Don't forget to import Webkit where it needs to be imported.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With