A Better Architecture to Implement Forms in iOS Applications

A Better Architecture to Implement Forms in iOS Applications

How to implement a clean and maintainable Form Architecture with UITableView.
Su Ho
Su Ho
May 04, 2021
iOS Mobile

iOS developers are accustomed to working with UITableView to present a list of data on the screen. However, dealing with UITableViewDataSource protocol and its reusable cells to implement a form has always baffled iOS developers the most.

Let’s take a look at this registration form:

An example of a registration form
An example of a registration form

Based on the UI above, there are different types of cells on the registration form namely: an input text cell, a date picker cell, a toggle cell, and a selectable cell. If a developer implements it in a standard way, the project will have tons of code in cellForRow function, for example:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    guard let item = viewModel.cellItem(at: indexPath) else { return UITableViewCell() }
    switch item.type {
    case .fullName:
        let cell = tableView.dequeue(TextInputTableViewCell.self)
        cell.configure(with: item)
        return cell
    case .password:
        let cell = tableView.dequeue(TextSecureInputTableViewCell.self)
        cell.configure(with: item)
        return cell
    case .birthday:
        let cell = tableView.dequeue(DateInputTableViewCell.self)
        cell.configure(with: item)
        return cell
    case .enabled2FA:
        let cell = tableView.dequeue(ToggleInputTableViewCell.self)
        cell.configure(with: item)
        return cell
    case .country:
        let cell = tableView.dequeue(SelectableInputTableViewCell.self)
        cell.configure(with: item)
        return cell
    // and more cases...
    }
}

In this post, we will show how to create a clean and maintainable UITableView by using protocols with examples so that the function cellForRow will be reduced to only a few lines of code.

UITableView and Protocol Oriented Cells

FormField

As mentioned, when a form has to support different types of cells, all the configuration for the cell happens in the cellForRow function, usually based on the index path. The downside is that when updating (adding, removing, or changing its position) multiple cell types, it will make the code complex and hard to maintain.

The FormField protocol will help iOS developers to write clean and maintainable table view code by separating the configuration for each type of cell.

import UIKit

protocol FormField: AnyObject {

    var height: CGFloat { get }

    func register(for tableView: UITableView)
    func dequeue(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell
}

When creating a new cell to show on the table view, we just need to create a field and make it conform to this protocol.

Let’s look at the implementation:

import UIKit

final class TextInputFormField {

    var viewModel: TextInputViewModel

    init(viewModel: TextInputViewModel) {
        self.key = key
        self.viewModel = viewModel
    }
}

// MARK: - FormField

extension TextInputFormField: FormField {

    var height: CGFloat { 44.0 }

    func register(for tableView: UITableView) {
        tableView.register(TextInputCell.self, forCellReuseIdentifier: "TextInputCell")
    }

    func dequeue(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "TextInputCell", for: indexPath) as! TextInputCell
        cell.configure(viewModel)
        return cell
    }
}
  • TextInputCell is a UITableViewCell that contains a label and a text field.
  • TextInputViewModel is a view model that will be used to configure the TextInputCell

Here is the configure function in TextInputCell:

extension TextInputCell {

    func configure(_ viewModel: TextInputViewModel) {
        titleLabel.text = viewModel.title
        textField.placeholder = viewModel.title
        textField.text = viewModel.value
        textField.isSecureTextEntry = viewModel.isSecure
    }
}

Creating the base controller

In addition to the FormField protocol, we will need to have a base controller FormViewController that will implement UITableViewDataSource and UITableViewDelegate.

This base controller can then be used in different modules by setting the right data into the fields attribute.

class FormViewController: UIViewController {

    let tableView = UITableView(frame: .zero, style: .grouped)

    var fields: [FormField] = [] {
        didSet {
            for field in fields {
                field.register(for: tableView)
            }
        }
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.delegate = self
        tableView.dataSource = self
    }
}

// MARK: - UITableViewDataSource

extension FormViewController: UITableViewDataSource {

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return dataSource.fields.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let field = dataSource.fields[indexPath.row]
        return field.dequeue(for: tableView, at: indexPath)
    }
}

// MARK: - UITableViewDelegate

extension FormViewController: UITableViewDelegate {

    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        let field = dataSource.fields[indexPath.row]
        return field.height
    }
}

With the base controller now defined, we can now, for example, create a module called FormDemo to create a registration form.

final class FormDemoViewController: FormViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        fields = [
            TextInputFormField(viewModel: .init(title: "Username")),
            TextInputFormField(viewModel: .init(title: "Password", isSecure: true))
        ]
    }
}

Here is the result:

Registration form with username and password fields
Registration form with username and password fields

Create a Toggle Cell

In the event that it would be necessary to add another type of cell in the table view, we can just create another field type and make it conform to FormField .

For example, let’s say that we want to create a toggle cell:

import UIKit

final class ToggleInputFormField {

    var viewModel: ToggleInputViewModel

    init(viewModel: ToggleInputViewModel) {
        self.viewModel = viewModel
    }
}

// MARK: - FormField

extension ToggleInputFormField: FormField {

    var height: CGFloat { 44.0 }

    func register(for tableView: UITableView) {
        tableView.register(ToggleInputCell.self, forCellReuseIdentifier: "ToggleInputCell")
    }

    func dequeue(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "ToggleInputCell", for: indexPath) as! ToggleInputCell
        cell.configure(viewModel)
        return cell
    }
}

ToggleInputCell only contains a label and a switch.

Then let’s update the fields attribute in FormDemoViewController:

final class FormDemoViewController: FormViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        fields = [
            TextInputFormField(viewModel: .init(title: "Username")),
            TextInputFormField(viewModel: .init(title: "Password", isSecure: true))
            ToggleInputFormField(viewModel: .init(title: "Remember me"))
        ]
    }
}
Registration form with username, password and remember me fields
Registration form with username, password and remember me fields

So with a protocol and a base controller, we can easily create a clean, maintainable and flexible table view. We now just need to care about the attribute fields in the class FormViewController.

Sections

So what about sections? Technically, we can do the same like FormField for sections:

protocol FormSection: AnyObject {

    var header: FormHeader?
    var footer: FormFooter?
    var fields: [FormField]

    init(header: FormHeader? = nil, footer: FormFooter? = nil, fields: [FormField])
}

Then let’s update the class FormViewController by replacing fields by sections.

class FormViewController: UIViewController {

    let tableView = UITableView(frame: .zero, style: .grouped)

    var sections: [FormSection] = [] {
        didSet {
            for section in sections {
                for field in section.fields {
                    field.register(for: tableView)
                }
            }
        }
    }
}

Conclusion

After working with a registration form, we finally found an easy way to implement a form with good architecture, readability and maintainability. Protocols, in this case, are the perfect tool to implement forms.

By introducing two protocols (FormField and FormSection) and a base controller (FormViewController), iOS developers can easily handle any layout changes or updates to input cells because the developer now only needs to worry about updating the sections and fields attributes to reflect the changes.

The code presented in this blog post can be found on Github. In the repository, we added more code to easily handle the data and also get data from the form. If you have any questions, issues, or features, issues and pull requests are welcome!

If this is the kind of challenges you wanna tackle, Nimble is hiring awesome web and mobile developers to join our team in Bangkok, Thailand, Ho Chi Minh City, Vietnam, and Da Nang, Vietnam✌️

Join Us

Recommended Stories:

Accelerate your digital transformation.

Subscribe to our newsletter and get latest news and trends from Nimble