Multiple UITableViewCells in UITableView
Learn how to add multiple UITableViewCells in a single UITableView with Swift.
This tutorial is focusing on how to add multiple UITableViewCells inside UITableView properly with the minimum code possible.
For this tutorial, I have picked the scenario where you have an app that contains screens like Contact Us, Register, Login etc. Simply said, you need to show forms on multiple screens with multiple types of cells. Of course, you can apply it to any other scenario where you need multiple UITableViewCells in UITableView.
Before we start there are a couple of things you show know:
For a better understanding, I have created a sample GitHub project which you can download here.
Creating the UITableViewCells
First, we will start with the UITableViewCells creation. If you open the project you will see various cells created under the folder Cells which contain input fields, dropdowns, action buttons, multi-line input fields. For presentation purposes, I will use BaseCell and InputCell.
We will start by creating one master cell (named BaseCell) which will handle all the job for the children. BaseCell is used to store functions that are mutual in all its children. Then we will use those function as overrides in the child cells. This cell won’t have a UI but will take the suitable one from its children.
BaseCell
import UIKitclass BaseCell: UITableViewCell { //MARK: Internal Properties
var type: CellType!
var pickerOptions: [String]!{
didSet{
pickerOptionsSet()
}
} var textChangedBlock: ((String) -> Void)?
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
func setForm(title: String, placeholder: String, keyboardType: UIKeyboardType){
set(title: title, placeholder: placeholder, image: "", secureEntry: false, keyboardType: keyboardType)
} func set(title: String, placeholder: String, image: String, secureEntry: Bool, keyboardType: UIKeyboardType){
setTitle(title: title)
setPlaceholder(placeholder: placeholder)
setKeyboardType(type: keyboardType)
setImage(image: image)
setSecureEntry(isSecure: secureEntry)
} func setTitle(title: String){}
func setPlaceholder(placeholder: String){}
func setKeyboardType(type: UIKeyboardType){}
func setSecureEntry(isSecure: Bool){}
func setImage(image: String){}
func setTextAlignment(textAlignment: NSTextAlignment){}
func pickerOptionsSet(){}
}
As you can see, I am creating one main function called set() which contains parameters that I need in order to “feed” all the children cells with data. Also, you can see another function called setForm() which is an example of a helper function if you don’t need to call some of the parameters. I also create helper functions for each parameter and then override them in the cell that requires that type of data. CellType is presented in the next section below.
Now, when we have the master cell in place we can start creating the children. As I have mentioned above, I will only present one child cell to keep things short, and you can follow the same flow for creating other child cells that you need. I still strongly recommend downloading the example project from Github.
InputCell
import UIKitclass InputCell: BaseCell { //MARK: Private Properties
@IBOutlet fileprivate weak var titleLbl: UILabel!
@IBOutlet fileprivate weak var inputTxt: UITextField! override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setTitle(title: String) {
titleLbl.text = title
}
override func setPlaceholder(placeholder: String) {
inputTxt.placeholder = placeholder
}
override func setKeyboardType(type: UIKeyboardType) {
inputTxt.keyboardType = type
}
@IBAction func textDidChange(textField: UITextField){
if let txt = textField.text{
textChangedBlock?(txt)
}
}
}
extension InputCell: UITextFieldDelegate{
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
return textField.resignFirstResponder()
}
}
The InputCell is a cell that shows us a UITextField and a UILabel above it which acts as a cell title. As you can see, it inherits from the BaseCell and we override only the helper methods that we need inside the InputCell class. So, we need to populate this cell with a title, a placeholder for the UITextField, and also tracking the change of the input by adding a closure in the BaseCell called textChangedBlock?().
That’s it from the cell creation, now we continue with creating the enum types.
Enum Types
The enum types are here to help us organize the code properly and I highly suggest that you use enumerations in any situation possible. In our case, you can imagine them as Settings for our cells. I will need 2 enum types for this tutorial:
- CellType — all the UI settings for the child cell
- FormCellType — all the settings needed for populating the cells with data
CellType
import Foundation
import UIKitenum CellType{
case input
case inputLong
case inputImage
case dropdown
case button
func getHeight() -> CGFloat{
switch self {
case .input, .dropdown, .button: return 80
case .inputLong: return 115
case .inputImage: return 65
}
}
func getClass() -> BaseCell.Type{
switch self {
case .input: return InputCell.self
case .inputLong: return InputLongCell.self
case .dropdown: return DropdownCell.self
case .button: return ButtonCell.self
case .inputImage: return InputImageCell.self
}
}
}
As mentioned above, we will use the CellType for deciding the UI. Currently, you can see the getHeight() function which will return the height of each cell type, and also getClass() which returns the type of the cell for the given case.
FormCellType
import Foundation
import UIKitenum FormCellType{
case name
case email
case username
case pass
case contactTypes
case work
case message
case send func getTitle() -> String{
switch self {
case .name: return "Name"
case .email: return "Email"
case .work: return "Work"
case .message: return "Message"
case .send: return "Send"
case .contactTypes: return "Pick Contact Type"
case .username: return "Username"
case .pass: return "Password"
}
}
func placeholder() -> String{
switch self {
case .name: return "Enter your name"
case .email: return "Enter your email address"
case .work: return "Enter your company place"
case .message: return "Write us a message (optional)"
case .username: return "Enter Username"
case .pass: return "Enter Password"
default: return ""
}
}
func image() -> String{
switch self {
case .username: return "form-username"
case .email: return "form-email"
case .pass: return "form-password"
default: return ""
}
}
func keyboardSecure() -> Bool{
switch self {
case .pass: return true
default: return false
}
} func keyboardType() -> UIKeyboardType{
switch self {
case .email: return .emailAddress
default: return .default
}
}
func pickerOptions()->[String]{
switch self {
case .contactTypes:
return ["Advertising on Site", "General Enquiries", "Feedback", "Account Enquiries"]
default: return []
}
}
func cellType() -> CellType{
switch self {
case .message: return .inputLong
case .send: return .button
case .username, .pass, .email: return .inputImage
case .contactTypes: return .dropdown
default: return .input
}
}
}
FormCellType is the enum type where you need to define the fields that you are going to be using in the UITableView and its settings like title, placeholder, keyboardType etc. Also, pay attention of the cellType() function where we decide the CellType of the case.
UIViewController Flow
Let’s see how to combine everything that we have learned so far. I will start by creating a master controller. The purpose of creating a master controller is to save you from writing repetitive code, easy code reuse, and keeping your main classes clean and organized. In our case, all of the controllers that need multiple cells with inherit from BaseController. Here, we will store the UITableViewCellDelegate and UITableViewCellDataSource methods. Let me demonstrate what is going on here…
import UIKitclass BaseController: UIViewController { var cellTypes = [FormCellType]()
override func viewDidLoad() {
super.viewDidLoad()
}
func currentCell(c: BaseCell, index: Int){
}
}extension BaseController: UITableViewDelegate{
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
let c = cellTypes[indexPath.row]
return c.cellType().getHeight()
}
}extension BaseController: UITableViewDataSource{
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return cellTypes.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let c = cellTypes[indexPath.row]
let cellClass = c.cellType().getClass()
let cell = tableView.dequeueReusableCell(withIdentifier: cellClass.cellReuseIdentifier(), for: indexPath) as! BaseCell
cell.set(title: c.getTitle(), placeholder: c.placeholder(), image: c.image(), secureEntry: c.keyboardSecure(), keyboardType: c.keyboardType())
cell.type = c.cellType()
if cell.type == .dropdown{ cell.pickerOptions = c.pickerOptions() }
currentCell(c: cell, index: indexPath.row) return cell
}
}
Starting from the top, cellTypes is an array that stores FormCellType enum values. We will declare in the master controller but will populate it in our child view controllers. You see how easily the height of the cell is passed to the heightForRowAt() delegate method. Same goes for the cellForRowAt() data source method, where you will just need to initialize the BaseCell and provide the reuseIdentifier from the child cell. The currentCell() function will be overridden whenever you need your cellForRowAt() data source method in your children controllers. That’s it for setting up the data source and delegate methods.
Now, all you need to do is just populate the cellTypes array in the child controllers with the cells you need, and it will fill in the UITableView instantly. For this tutorial, I have used only 1 child controller with 2 different forms, but you can easily use x number of child controllers and only fill in the cellTypes array. Look how short our controller would be…
ViewController
import UIKitclass ViewController: BaseController {
//MARK: Private properties
@IBOutlet fileprivate weak var mainTableView: UITableView! override func viewDidLoad() {
super.viewDidLoad()
cellTypes = [.name, .work, .contactTypes, .message, .send]
setupUI()
} override func currentCell(c: BaseCell, index: Int) {
let type = cellTypes[index]
if type == .contactTypes{
let cell = c as! DropdownCell
cell.actionBlock = { (options) in
print(options)
}
}
}
}private extension ViewController{
func setupUI(){
for type in cellTypes{
mainTableView.registerNibForCellClass(type.cellType().getClass())
}
}
@IBAction func onRegisterButton(btn: UIButton){
cellTypes = [.email, .username, .pass, .send]
setupUI()
mainTableView.reloadData()
}
@IBAction func onContactUsButton(btn: UIButton){
cellTypes = [.name, .work, .contactTypes, .message, .send]
setupUI()
mainTableView.reloadData()
}}
There you have it. You got a nicely organized controller with multiple UITableViewCells support in a single UITableView. 🙂
In order to register the UITableViewCells to the UITableView, I am using 2 extensions…
UITableView+Extensions.swift
import UIKitextension UITableView {
func registerNibForCellClass(_ cellClass: UITableViewCell.Type) {
let cellReuseIdentifier = cellClass.cellReuseIdentifier()
let nibCell = UINib(nibName: cellReuseIdentifier, bundle: nil)
register(nibCell, forCellReuseIdentifier: cellReuseIdentifier)
}
}
UITableViewCell+Extensions.swift
import UIKitextension UITableViewCell {
class func cellReuseIdentifier() -> String {
return "\(self)"
}
}