Introduction

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.

Limitations of UITableView

UITableView 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:

  1. Dynamic HeightsUITableView 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.
  2. Constraint Management: iOS's Auto Layout system can be complex, especially when constraints need to be dynamically adjusted based on content changes. This often leads to constraint conflicts and layout issues.
  3. Performance: Updating the UI in real-time to accommodate dynamic content can impact performance, making it essential to use efficient techniques like debouncing updates.

Step-by-Step Integration

  1. Create a custom 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)
    }
}
  1. Initialise your 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)
])
  1. Extend your 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: