I like this pattern enabled by Swift:
let sections = [(title: "Alpha", rows: ["A", "B", "C"]),
               (title: "Numeric", rows: ["1", "2", "3"])]()
Then in cell for row at index path:
let data = sections[indexPath.section].rows[indexPath.row]
Thanks to Martin R's answer for inspiration. My actual implementation uses another pattern that I have to use disparate cell classes and data types like this:
protocol BaseViewModel {
    var cellIdentifier: String
}
class BaseCell: UITableViewCell {
    func setupWith(viewModel: BaseViewModel) {}
}
class SpecificCell: BaseCell {
    override func setupWith(viewModel: BaseViewModel {
        if let viewModel = viewModel as? SpecificCellViewModel {
            // set properties from object conforming to my vm protocol
        }
    }
}
Implementation:
let sections: [(title: String, rows: [BaseViewModel])]
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let viewModel = sections[indexPath.section].rows[indexPath.row]
    let cell = let tableCell = tableView.dequeueReusableCell(withIdentifier: viewModel.cellIdentifier , for: indexPath) as? BaseTableViewCell
    tableCell?.setupWith(viewModel: viewModel)
    return tableCell ?? UITableViewCell()
}
Happy coding! Let me know with a comment if this needs further elaboration or has any errors.