I ❤ Swift [Part 2] – Multiple UITableViewCells in UITableView

swift-multiple-uitableviewcells-uitableview

This is a Part 2 of the “I ❤ Swift” series that I write to make you love Swift even more by showing you some tips and tricks. Also, it will make you pay attention more to the code organization through your projects. 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:

multiple-uitableviewcells-uitableview

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 UIKit

class 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 UIKit

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

  1. CellType – all the UI settings for the child cell
  2. FormCellType – all the settings needed for populating the cells with data

CellType

import Foundation
import UIKit

enum 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 UIKit

enum 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 UIKit

class 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 UIKit

class 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 UIKit

extension 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 UIKit

extension UITableViewCell {
    
    class func cellReuseIdentifier() -> String {
        return "\(self)"
    }
}

Multiple UITableViewCells in UITableView

This tutorial has shown you how to load multiple UITableViewCells with various cell types. I hope that you have learned something new today and that it helped you improve your code organization. If you liked this post, please don’t forget to share it and maybe help others. 🙂

SWIFT – Move to the next UITextField by hitting Return

Follow me on Medium, for more interesting Swift programming tutorials.

Leave a Reply

Your email address will not be published. Required fields are marked *