Alex Brie

I am Alexandru Brie - iOS developer / solopreneur. This is my real blog.

Logo

Blog

Tech (english) 29 Oct 2018

8 May 2018

iOS dev story - A declarative approach for readable UITableViewControlllers

by Alex

Search the docs or most online tutorials on building and populating a non-trivial static UITableView in iOS, such as the one used for a settings screen in your app, and you’ll start to notice a lot of duplication and switches over enums and cell types. Here’s what I mean:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let basicCellIdentifier = "BasicCellIdentifier"
    let detailsCellIdentifier = "DetailsCellIdentifier"

    if (indexPath as NSIndexPath).section == MyAccountViewController.SECTION_UTILS {
          let cell = tableView.dequeueReusableCell(withIdentifier: basicCellIdentifier)
          if (indexPath as NSIndexPath).row == MyAccountViewController.ROW_UTILS_DASHBOARD {
                    cell?.imageView?.image = UIImage(named: "accountDashboard")
                    cell?.textLabel?.text = "Account Dashboard"
          } else if (indexPath as NSIndexPath).row == MyAccountViewController.ROW_UTILS_PERSONAL_INFO {
                    cell?.imageView?.image = UIImage(named: "accountPersonalInformation")
                    cell?.textLabel?.text = "Personal Information"
          } else if (indexPath as NSIndexPath).row == MyAccountViewController.ROW_UTILS_PROFILE {
                    cell?.imageView?.image = UIImage(named: "accountProfile")
                    cell?.textLabel?.text = "Profile"
          } else if (indexPath as NSIndexPath).row == MyAccountViewController.ROW_UTILS_EXTRAS {
                    cell?.imageView?.image = UIImage(named: "accountExtras")
                    cell?.textLabel?.text = "Extras"
          }

          return cell!
    } else if (indexPath as NSIndexPath).section == MyAccountViewController.SECTION_SETTINGS {
          let cell = tableView.dequeueReusableCell(withIdentifier: detailsCellIdentifier)
          cell?.imageView?.image = UIImage(named: "accountPushNotifications")
          cell?.textLabel?.text = "Push Notifications"
          cell?.detailTextLabel?.text = ""

          return cell!
    } else {
...

I’ll stop here before you ragequit or go blind. You got the idea, just like you probably guessed that handling the selection of a cell involves the same overly verbose and unreadable mess.

For the settings menu of the new app I’m working on, I went with a different approach:

A declarative UITableViewController

First, a simple object with an array of what sections and what rows we’ll have in the table

@objc class ContentProvider: NSObject{
   let contentsDescriptor : [SectionType] = [
       ("Info" , [
           SimpleRow(title: "User email", subtitle: "", action: nil, populate: #selector(ContentProvider.currentUserEmail(_:))),
           SimpleRow(title: "User id", subtitle: "", action: nil, populate: #selector(ContentProvider.currentUserId(_:))),
           SimpleRow(title: "Endpoint", subtitle: "", action: #selector(ContentProvider.editEndpoint), populate: #selector(ContentProvider.showEndpoint(_:))),
           ]
       ),
       ("Others" , [
           SimpleRow(title: "Reset All user defaults", subtitle: nil, action: #selector(ContentProvider.resetUserDefaults), populate: nil),
           SimpleRow(title: "Force Crashlytics Crash", subtitle: nil, action: #selector(ContentProvider.forceCrashlyticsCrash), populate: nil)
           ]
       )
   ]
   ...

SimpleRow and SectionType are simple data structures used for type clarity. The methods pointed to by the selectors implement the actual logic of displaying data and handling the tap on the cells (see below).

  struct SimpleRow {
      var title: String, subtitle: String?, action: Selector?, populate: Selector?
  }
  typealias SectionType = (title: String, rows: [SimpleRow])

  // MARK: - Rows logic
  @objc func currentUserEmail(_ cell: UITableViewCell) {
       let user = UserSession.shared.loggedInUser()
       cell.detailTextLabel?.text = user?.email
  }
  //...

Now, I can have a completely generic view controller (maybe even one that inherits from UITableViewController) which uses a ContentProvider instance to display its contentsDescriptor array.

The classic cellForRowAt mess from above becomes now reusable, clean and easy to understand, and the entire view controller fits in around 40 lines of code.

class DebugViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
    //...
    var contentsArray : ContentProvider!
    override func viewDidLoad() {
        super.viewDidLoad()
        contentsArray = ContentProvider(presentingController: self)
        tableView.dataSource = self
        tableView.delegate = self
    }

    // MARK: - TableView
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let staticCellIdentifier = "CellIdentifier"
        let cellData = contentsArray.contentsDescriptor[indexPath.section].rows[indexPath.row]
        var cell = tableView.dequeueReusableCell(withIdentifier: staticCellIdentifier)
        cell?.textLabel?.text = cellData.title
        cell?.detailTextLabel?.text = cellData.subtitle
        if cellData.populate != nil {
            contentsArray.perform(cellData.populate, with: cell)
        }

        return cell!
    }

    func numberOfSections(in tableView: UITableView) -> Int {
        return contentsArray.contentsDescriptor.count
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return contentsArray.contentsDescriptor[section].rows.count
    }

    func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        return contentsArray.contentsDescriptor[section].title
    }

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let cellData = contentsArray.contentsDescriptor[indexPath.section].rows[indexPath.row]
        if cellData.action != nil {
            contentsArray.perform(cellData.action)
        }
    }
}

The code above is certainly not foolproof, but it gets the job done without any constants or assumptions on the number of sections, cells, their ids, etc.

By splitting the functionality into a tiny view controller and a contents array which describes the sections, rows, and their wanted behavior (when initializing them and when selecting), we achieved a concise, clear, reasonably flexible and easily reusable code for static tables with basic cells.


If this helps give coding ideas to at least one other dev, I’ll be ecstatic. Remember to drop some feedback on twitter: @alexbrie

tags: