Best Practices, Tutorial Resources

Enumified TableView with Dynamic Prototype Cells in Swift

How I refactored my app’s Settings screen code to be much more flexible and readable, while unlocking a more complicated TableView at the same time! Win-Win!

I stumbled upon this amazing post by Frédéric Adda over at Nova Era called Structure Your UITableView Better with Structs and Enums in Swift as I was searching for a way to refactor the Settings page in my app, BB Links, to make it easier to add in new settings options anywhere, or change around any items/sections, without needing to manually worry about the section and row index numbers.

The linked post above explains how you can use enums and structs to replace the indexes for section and rows–it’s really cool!!! 😎 Exactly what I was looking for! Below is my implementation of it for my app’s Settings screen, and I took this concept even further later on in so I could easily have different Dynamic Prototype Cells! đŸ˜±

With this change, I can move any item to any section and change the order of sections and items all within a couple of seconds! To do that with section and row indexes requires a bunch of index changes which can get confusing to keep track of.

Here’s how I set it up in my app, first implementing the enums and structs, as FrĂ©dĂ©ric’s article suggested:

private enum SectionType {
  case Account
  case Other
}

private enum Item {
  case ManageAccounts
  case ActiveAccount
  case DefaultCountry
  case AboutMe
  case Donate
  case RateApp
  case ShareApp
}

private struct Section {
  var type: SectionType
  var items: [Item]
}

First, I created the SectionType enum which represents the different sections I want to have in the Settings screen. I have an Account section and Other.

Next, I create the Item enum which contains all of the different tableView rows I have in the Settings screen.

Lastly, a Section struct is created which is identified as containing a SectionType enum and an array of Item enums.

Once those three components were set up, the next step was to create an instance of an array of Section structs, which will hold the various sections in the Settings screen.

class SettingsViewController: UIViewController {
  private var sections = [Section]()
  ...
}

In the viewDidLoad() method, I assign the items that belong in each section:

override func viewDidLoad() {
  super.viewDidLoad()
  // Setup sections/rows for table
  sections = [
    Section(type: .Account, items: [.ManageAccounts, .ActiveAccount, .DefaultCountry]),
    Section(type: .Other, items: [.AboutMe, .Donate, .RateApp, .ShareApp])
  ]
  ...
}

The Account section contains the ManageAccounts, ActiveAccount and DefaultCountry tableview rows, and the Other section contains the remaining four rows.

Next up…the UITableViewDataSource implementation which I have as an extension to my SettingsViewController class.

// MARK: - UITableViewDataSource
extension SettingsViewController: UITableViewDataSource {
  func numberOfSectionsInTableView(tableView: UITableView) -> Int {
    return sections.count
  }

  func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return sections[section].items.count
  }

  func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
    switch sections[section].type {
    case .Account:
      return "Account"
    case .Other:
      return nil
    }
  }
  ...
}

For tableView:numberOfSectionsInTableView:, you just return the count of your sections array, which will be 2 as it contains my two Section structs.

for tableView:numberOfRowsInSection:, return the count of items in each section, and for tableView:titleForHeaderInSection:, I setup a switch statement to switch through the types, returning the name for each section. (For Other, I just wanted it to be blank, so I didn’t return a title).

Before I show how I used these enums and structs to help me easily implement three different dynamic protoypes, here’s how I implemented the UITableViewDelegate methods:

// MARK: - UITableViewDelegate Methods
extension SettingsViewController: UITableViewDelegate {
  // Goes to product detail when cell tapped
  func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
    switch sections[indexPath.section].items[indexPath.row] {
    case .ManageAccounts:
      performSegueWithIdentifier("ManageCoachIDs", sender: self)
    case .ActiveAccount:
      performSegueWithIdentifier("DefaultCoachID", sender: self)
    case .DefaultCountry:
      performSegueWithIdentifier("DefaultCustomerCountry", sender: self)
    case .AboutMe:
      performSegueWithIdentifier("About", sender: self)
    case .Donate:
      performSegueWithIdentifier("Donate", sender: self)
    case .RateApp:
      rateApp()
    case .ShareApp:
      shareAppFrom(indexPath)
    }
    tableView.deselectRowAtIndexPath(indexPath, animated: true)
  }
}

I just switch between the section and row using the sections array, and perform my segues or functions for each Item. It’s all very clear what is what thanks to the Item enum setup earlier! I love it! ❀ Without this setup, you’d just have combinations of sections and row index numbers and you’d need to keep track of which is which with comments. And don’t even think it would be simple to switch them around. With the way this is setup, it really doesn’t matter which order you put the case Items…it’s all handled by the way you place them in your section variable up in the viewDidLoad()! Brilliant! 🙌

For the final part, I’ll show how I easily implemented my three dynamic prototype cells, which are just a simple Basic Cell, a Subtitle Cell and Right Detail Cell (setup as such in the storyboard with appropriate cell identifiers):

Here’s how my storyboard is setup:

Storyboard

And implementation of tableView:cellforRowAtIndexPath::

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
  // Setup a cellIdentifer string to store the cell reuse identifier you want to use for each row.
  var cellIdentifier: String

  // Switch through each row and set the appropriate cell reuse identifier
  switch sections[indexPath.section].items[indexPath.row] {
  case .ManageAccounts, .AboutMe, .Donate:
    cellIdentifier = "BasicCell"
  case .RateApp, .ShareApp:
    cellIdentifier = "SubtitleCell"
  case .ActiveAccount, .DefaultCountry:
    cellIdentifier = "RightDetailCell"
  }

  // Populate your cell reuse identifier into the cell
  let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath)

  // Switch through each cell, and implement the labels/setup for each row
  // The order of the cases is irrelevant!
  switch sections[indexPath.section].items[indexPath.row] {
  case .ManageAccounts:
    cell.textLabel?.text = "Manage Coach IDs"
  case .ActiveAccount:
    cell.textLabel?.text = "Active Coach ID"
    let coachID = dataModel.getActiveCoachID()
    if coachID != "" {
      cell.detailTextLabel?.text = coachID
    } else {
      cell.detailTextLabel?.text = ""
    }
  case .DefaultCountry:
    cell.textLabel?.text = "Default Country"
    cell.detailTextLabel?.text = dataModel.getDefaultCountry()
  case .AboutMe:
    cell.textLabel?.text = "About the Developer đŸ’ȘđŸ€“"
  case .Donate:
    cell.textLabel?.text = "☕☕☕?"
  case .RateApp:
    cell.textLabel?.text = "Rate BB Links ⭐⭐⭐⭐⭐"
    if let reviewCount = reviewCount {
      cell.detailTextLabel?.text = "\(reviewCount) people have rated this version"
      UIView.animateWithDuration(0.3, animations: {
        cell.detailTextLabel?.alpha = 1.0
      })
    } else {
      cell.detailTextLabel?.text = " "
      cell.detailTextLabel?.alpha = 0.0
    }
    cell.accessoryType = .None
  case .ShareApp:
    cell.textLabel?.text = "Share BB Links đŸ—ŁđŸ‘„"
    cell.detailTextLabel?.text = "Know a coach who would love this app?"
    cell.accessoryType = .None
  }

  // Return the cell
  return cell
}

I love the way this worked because it’s clear exactly which row I’m working with, and I can quickly change the cell type by moving around the row Item where the cellIdentifier variable string is set in the first switch statement and changing its cell implementation details! So. Much. Clearer! 🙌

With all of this setup the way it is, adding new sections or rows is a cinch! I could totally rearrange this tableView to look completely different switching a couple of lines of code. The normal way would be way more difficult.

Here’s the finished result in-app:

Settings Screen Result

It’s now setup for quick modifications in the future! 🎉

If you ask me, enumified tableViews with dynamic prototype cells rock! 😎

Standard

18 thoughts on “Enumified TableView with Dynamic Prototype Cells in Swift

  1. Hey Shyam! Glad it’s useful in some way, and I’m sure eventually it will all make sense. 😀

    With static cells, they only work in storyboards as they don’t use a data source. So this wouldn’t work as everything above is to create the table’s data source. To populate static cells, you would just manually populate their data directly in the storyboard. Static cells do have some limitation as to what’s possible with them, which I think is why you’re asking if this would work with them.

    I say give dynamic cells a shot in code and see if you can get close to what you’re looking for.

    Like

    • Hey Shyam sorry I missed this!

      Can you go to your storyboard and change the style to Grouped instead of Plain? That should do it, although it will change the style of your section headers.

      An alternative way to get rid of the extra blank rows while keeping the table style as Plain is to add this line of code to your viewDidLoad (assuming you already have an outlet hooked up to your tableview):

      tableView.tableFooterView = UIView()

      This just adds a blank footer to your table’s bottom so instead of it filling the space with blank rows (since by default there is no footer on a Plain style tableview), it fills it with the blank footer you just added.

      Let me know how those options work!

      Like

  2. Hey Justin. It’s me who has to be sorry for missing this for an even longer time.

    Thanks very much for the help, though. Coming to your suggestions, I had tried both and below is my observation.

    Option 1: Changing to grouped from plain, was straight forward. It did the trick for me. I’ll be following it.

    Option 2: Adding a blank footer, removed the cells but filled the blank space with white.

    Thanks again.

    Like

  3. Hi Justin, its my turn for being sorry, to not able to respond quickly on your suggestion.

    But, I have now tried both and would go with the first one.

    Option 1: Changing to Grouped from Plain, did the trick as I wanted it to.

    Option 2: Adding a blank footer, clears the cell, but the space is filled with white!!

    Finally, thanks again for your continued support. 🙂

    Liked by 1 person

  4. Yup…what you would need to do is make a new custom nib/xib with a tableview cell and add your custom elements such as a switch and give it a reuse identifier. For the switch to work, you would need to create either a delegate or closure for the cell so you can react to the switch being toggled on or off.

    This post was an excellent one to show how to do it with closures: http://candycode.io/how-to-properly-do-buttons-in-table-view-cells-using-swift-closures/ and there was an original post that did it with delegates: http://candycode.io/how-to-properly-do-buttons-in-table-view-cells/.

    Like

  5. Hi Justin,

    I did go through those blogs, and was able to include quite a few elements. But I had to have dynamic footers for my sections. I used tableView.reloadSections and tableView.reloadRowsAtIndexPaths, and they seem to work as I wanted. But, the tableView is messed up as I scroll. Not sure, if you have encountered this, but there is either something wrong with how these methods work, or I must be using them in a wrong manner.

    I would like to mention that if they are used only once, it’s fine. Only multiple use of the above methods, cause UI discrepancy.

    Like

      • ‘reloadSections’ and ‘reloadRowsAtIndexPaths’ are the methods I used to update the values of the cells, if there is a change in the values behind the scenes. Example, a UISwitch needs to be turned off/on based on some other criteria.

        The rows get jumbled up into different sections. So, a row which was supposed to be in section one, might end up in section two, and if we scroll again, it might move to section three, or return to section one.

        I don’t see a way of sharing the video of this, else I would have. It is best explained, while in operation. Or, you can try it yourself. I believe it to be a bug, and assume that a radar has been filed already!

        Like

    • Any reason why you wouldn’t just use tableView.reloadData() instead to reload the entire table’s data?

      Are you doing any changes to the actual structure of the sections/rows once the view is loaded, or is the table’s datasource structure constant?

      Like

  6. As I had a few UISwitch elements, I needed the value to change accordingly and many other similar use cases, I figured out that these methods had to be used.

    Nope the structure doesn’t change!! It will be as it was setup.

    To be a bit more clear, let me explain it along with a snippet from my code..

    switch sections[indexPath.section].items[indexPath.row] {
    case .ShowCloudMusic:
    cell.callback = { (switch) -> Void in
    userDefaults.setBool(cell.value, forKey: cloudMusicKey)
    tableView.reloadSections(NSIndexSet(index: indexPath.section), withRowAnimation: .Automatic)
    }
    cell.switchCellLabel?.text = “Show Cloud Music”
    cell.switchCellSwitch.on = userDefaults.boolForKey(cloudMusicKey)
    cell.selectionStyle = .None

    So, this would be one of the cases, where I intend to convey the user, whether the UISwitch is on/off. I’d like to change the footer as well. And having tableView.reloadSections in the “callback” does that

    And, I have another scenario, when I need to modify the detailTextLabel, after segueing back. And this comes to the rescue again.

    So, would you advise to use tableView.reloadData? I don’t think it will do the trick. Actually, when I used it in lieu of tableView.reloadSections, not a single cell was visible!!

    Like

    • I’m having a hard time visualizing everything and seeing how it’s all connected. Is it possible to put it on dropbox or somewhere and emailing me a link to check it out? justin dot stanley at gmail dot com.

      Like

  7. Alex says:

    Hi!

    It’s a really good stuff. Thank you.

    Do you have any idea, how can we implement a search to the table of this king?

    Thanks.

    Like

    • Hmm…that would be a tough one. You need some sort of data source to be able to filter. The cells would all have textLabel’s, and those have text, so you could instead store those as a computer var on the Item enum and then set that down below in the switch where you set the cell’s text. But because there are sections and items in each section, to filter for search, you’d need to show/hide the appropriate matching cells as the user types. This would complicate things…

      Like

Leave a comment