When working with UITableView, developers often encounter various limitations, particularly when integrating dynamic content like third-party widgets. The challenge becomes more pronounced when the widget's content size isn't known upfront, necessitating real-time adjustments based on its actual size. This guide aims to address these challenges by walking through the process of seamlessly integrating a PlotlineWidget into a UITableView, ensuring it dynamically adjusts its height based on its content.
UITableViewUITableView is a powerful component of iOS development, allowing for the display of scrollable content in a list format. However, it comes with its own set of challenges, especially when dealing with dynamic content:
UITableView cells typically have static or pre-calculated heights. When integrating dynamic content, such as a widget that can expand or contract based on its data, managing the cell height dynamically becomes tricky.UITableViewCell to encapsulate PlotlineWidget.//
// PlotlineWidgetCell.swift
// DemoApp
//
// Created by SHUBH SARASWAT on 05/06/24.
//
import UIKit
import Plotline
class PlotlineWidgetCell: UITableViewCell {
var widget: PlotlineWidget?
var widgetHeight: CGFloat = 0
var onHeightDetermined: ((CGFloat) -> Void)?
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupWidget()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupWidget()
}
private func setupWidget() {
if widget == nil {
widget = PlotlineWidget(clientElementId: "native1")
widget?.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(widget!)
let topAnchor = widget!.topAnchor.constraint(equalTo: contentView.topAnchor)
let bottomAnchor = widget!.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
let leadingAnchor = widget!.leadingAnchor.constraint(equalTo: contentView.leadingAnchor)
let trailingAnchor = widget!.trailingAnchor.constraint(equalTo: contentView.trailingAnchor)
topAnchor.priority = .defaultHigh
bottomAnchor.priority = .defaultHigh
leadingAnchor.priority = .defaultHigh
trailingAnchor.priority = .defaultHigh
NSLayoutConstraint.activate([
topAnchor,
bottomAnchor,
leadingAnchor,
trailingAnchor
])
widget?.setPlotlineWidgetListener(plotlineWidgetListener: PlotlineWidgetListenerImpl { [weak self] width, height in
guard let self = self else { return }
widgetHeight = max(widgetHeight, height)
self.onHeightDetermined?(widgetHeight)
})
}
}
override func prepareForReuse() {
super.prepareForReuse()
widget?.removeFromSuperview()
widget = nil
setupWidget()
}
}
class PlotlineWidgetListenerImpl: NSObject, PlotlineWidgetListener {
private let onWidgetReadyClosure: (CGFloat, CGFloat) -> Void
init(onWidgetReady: @escaping (CGFloat, CGFloat) -> Void) {
self.onWidgetReadyClosure = onWidgetReady
}
func onWidgetReady(width: CGFloat, height: CGFloat) {
onWidgetReadyClosure(width, height)
}
}
UITableView and register the custom UITableViewCell we built above along with your regular cell.var tableView: UITableView!
tableView = UITableView()
tableView.delegate = self
tableView.dataSource = self
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
tableView.register(PlotlineWidgetCell.self, forCellReuseIdentifier: "PlotlineWidgetCell")
tableView.estimatedRowHeight = 50
tableView.rowHeight = UITableView.automaticDimension
view.addSubview(tableView)
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
UIView class to UITableViewDelegate and UITableViewDataSource, and implement the delegate methods like shown in the example below.var widgetHeight: CGFloat = 0 {
didSet {
if oldValue != widgetHeight {
debounceTableReload()
}
}
}
private var widgetHeightDebouncer: Timer?
// UITableViewDelegate and UITableViewDataSource methods
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 6
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Here, I wanted to put PlotlineWidget in the 3rd row, you can replace this with any position you want it to be.
if indexPath.row == 2 {
let cell = tableView.dequeueReusableCell(withIdentifier: "PlotlineWidgetCell", for: indexPath) as! PlotlineWidgetCell
cell.onHeightDetermined = { [weak self] height in
guard let self = self else { return }
if height > 0 && self.widgetHeight != height {
self.widgetHeight = height
}
}
return cell
} else {
let rowOffset = indexPath.row > 2 ? 1 : 0
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = "Row \\(indexPath.row + 1 - rowOffset)"
return cell
}
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
// Setting height for the widget's row to be equal to widgetHeight is widget is present, for other views it can be automatic dimension or any custom height yyou want.
if indexPath.row == 2 {
return widgetHeight > 0 ? widgetHeight : 0
} else {
return UITableView.automaticDimension
}
}
// Adding a debounced reload to make the tableView respond to the changes in widgetHeight
private func debounceTableReload() {
widgetHeightDebouncer?.invalidate()
widgetHeightDebouncer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: false) { [weak self] _ in
self?.tableView.beginUpdates()
self?.tableView.endUpdates()
}
}
Here is the full code for an example ViewController for a complete overview: