Using Swift Enums for Solid UICollectionViews and UITableViews


Let the Swift compiler help you build a rock-solid UICollectionView or UITableView and manage their layout structure through an enum. Tutorial and benefits

By Ariel Elkin

A good way to let the Swift compiler help you build a rock-solid UICollectionView or UITableView is to manage their layout structure through an enum. In this post, I’ll provide you with a basic implementation and discuss its benefits.

Implementation

For the purpose of illustration, let’s take a look at the following account view, which is taken from the app we developed for our venture Evino.

UICollectionView 1

We can utilize an enum to manage the sections shown in the UICollectionView above. The same pattern can also be applied to manage the items within each section (see section“Variable number of items” for more details).

The implementation looks like this:

private enum Section: Int {<br>    case UserDetails<br>    case Orders<br>    case AccountSettings<br><br>    static func count() -> Int {<br>        return Section.AccountSettings.rawValue + 1<br>    }<br>}<br>    <br>private enum OrdersItem: Int {<br>    case HeaderUpcomingOrders<br>    case Order<br>    case AllParcels<br>}<br><br>private enum AccountSettingsItem: Int {<br>    case DefaultPayment<br>    case DefaultAddress<br>    case NotificationSettings<br>}<br><br>extension ViewController: UICollectionViewDataSource {<br>    func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {<br><br>        guard let section = Section(rawValue: indexPath.section) else {<br>            assertionFailure()<br>            return UICollectionViewCell()<br>        }<br><br>        switch section {<br><br>        case .UserDetails:<br>            let cell = collectionView.dequeueReusableCellWithReuseIdentifier("UserDetailsCell", forIndexPath: indexPath) as! UserDetailsCell<br>            return cell<br><br>        case .Orders:<br>            let cell = collectionView.dequeueReusableCellWithReuseIdentifier("OrdersCell", forIndexPath: indexPath) as! OrdersCell<br>            return cell<br><br>        case .AccountSettings:<br>            let cell = collectionView.dequeueReusableCellWithReuseIdentifier("AccountSettingsCell", forIndexPath: indexPath) as! AccountSettingsCell<br>            return cell<br>        }<br>    }<br><br>    func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {<br>        return Section.count()<br>    }<br>}Code language: HTML, XML (xml)

Note that in my example code I implemented this pattern using a UICollectionView, but the same pattern can be applied to UITableView layouts.

Benefits

The usage of enum to manage layout structures offers some pretty neat advantages over other approaches:

  • Reorderings, additions, and deletions can be handled with ease: just change the order of the enum’s values.
  • No more hard-coded integer literals to represent section or item numbers.
  • Using the guard statement and the if let optional binding ensures that the collection view is always in a consistent state, thus avoiding common runtime errors.
  • Compare only existing section and item numbers safely and exhaustively using switch:
Switch

Discussion

  • We’re representing the collection view’s layout structure in an enumeration, which allows us to work with the collection view’s items and sections in a type-safe way.
  • Other approaches have been presented (see references below), but this blog post attempts to offer a more concise and robust approach, updated for Swift 2.
  • There’s no need to explicitly provide default integer values in the enum’s declaration. The enum’s first value will be assigned 0 by default, and the following values will auto-increment.

Providing the count

Some implementations provide the count of items by adding an enum case like this:

private enum Section: Int {<br>    case UserDetails<br>    case Orders<br>    case AccountSettings<br>    case NumberOfSections<br>}<br><br>func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {<br>    return Section.NumberOfSections<br>}Code language: HTML, XML (xml)

Clever, but I find that this case clashes with the original purpose of the enum. Another downside is that you’ll also have to match NumberOfSections whenever you switch on the enum, e.g. in cellForItemAtIndexPath.

In my implementation, you have to manually update the count() function, but only if the last item changes. A more elegant way to provide a count would rely on enums providing their number of cases natively. Sadly, as of Swift 2.1, this is not possible.

Take advantage of enum’s methods

Add methods and computed properties to your enum that provide relevant data for constructing the layout of your collection view.

For example, let it keep track of the reuse identifier associated with each cell:

private enum Section: Int {<br>    case UserDetails<br>    case Orders<br>    case AccountSettings<br><br>    static func count() -> Int {<br>        return Section.AccountSettings.rawValue + 1<br>    }<br><br>    var reuseIdentifier: String {<br>        switch self {<br>        case .UserDetails:<br>            return "UserDetails"<br>        case .Orders:<br>            return "Orders"<br>        case .AccountSettings:<br>            return "AccountSettings"<br>        }<br>    }<br>}<br><br>//Now we don't need to copy paste reuse identifier <br>//strings when calling your collection view's <br>//registerClass:forCellWithReuseIdentifier:<br><br>collectionView.registerClass(UserDetailsCell.self, forCellWithReuseIdentifier: Section.UserDetails.reuseIdentifier)<br>collectionView.registerClass(OrderCell.self, forCellWithReuseIdentifier: Section.Orders.reuseIdentifier)<br>collectionView.registerClass(AccountSettingsCell.self, forCellWithReuseIdentifier: Section.AccountSettings.reuseIdentifier)<br><br>//No need to copy paste them ever again!<br><br>func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {<br><br>    guard let section = Section(rawValue: indexPath.section) else {<br>        assertionFailure()<br>        return UICollectionViewCell()<br>    }<br><br>    var cell = collectionView.dequeueReusableCellWithReuseIdentifier(section.reuseIdentifier, forIndexPath: indexPath)<br><br>    switch section {<br><br>    case .UserDetails:<br>        cell = cell as! UserDetailsCell<br>        //update cell...<br>        return cell<br><br>    case .Orders:<br>        cell = cell as! OrderCell<br>        return cell<br><br>    case .AccountSettings:<br>        cell = cell as! AccountSettingsCell<br>        return cell<br>    }<br>}Code language: HTML, XML (xml)

Variable number of items

Usually, one of the sections in your collection view will contain a variable number of items. Consider this UI:

UICollectionView 2

You might be representing the UPCOMING ORDERS section in the following way:

private enum OrdersItem: Int {<br>    case HeaderUpcomingOrders<br>    case Order<br>    case AllParcels<br>}Code language: HTML, XML (xml)

Suppose that you must always display the UPCOMING ORDERS header, the All Parcels cell at the bottom of the section, and the list of orders, if any, in between. Currently the raw value of OrdersItem.AllParcels is 2, but we may need it to be 1 if there are no orders, or 10 if there are 8 orders (HeaderUpcomingOrders always being item number 0).

When the number of items is variable, you can’t use the enum’s default initializer, but you can implement a new initializer with the logic you need:

private enum OrdersItem: Int {<br>    case HeaderUpcomingOrders<br>    case Order<br>    case AllParcels<br><br>    //rawValue is the value passed by the collection view's<br>    //delegate methods, count is the count of Order items<br>    init(rawValue: Int, count: Int) {<br>        if rawValue == 0 {<br>            self = .HeaderUpcomingOrders<br>        }<br>        else if rawValue > count {<br>            self = .AllParcels<br>        }<br>        else {<br>            self = .Order<br>        }<br>    }<br>}Code language: HTML, XML (xml)

This ensures that HeaderUpcomingOrders always appears first, that AllParcels appears last, and that Orders appears in between.

Here’s how you’d roll it out:

func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {<br><br>    let section = Section(rawValue: indexPath.section)!<br><br>    var cell = collectionView.dequeueReusableCellWithReuseIdentifier(section.reuseIdentifier, forIndexPath: indexPath)<br><br>    switch section {<br><br>    case .Orders:<br><br>        guard let item = OrdersItem(rawValue: indexPath.item, count: orderArray.count) else {<br>            assertionFailure("incorrect order section item number")<br>            return UICollectionViewCell()<br>        }<br><br>        switch item {<br><br>        case .HeaderUpcomingOrders:<br>            cell = cell as! HeaderUpcomingOrdersCell<br>            return cell<br><br>        case .Order:<br>            cell = cell as! OrderCell<br>            cell.order = upcomingOrderArray[indexPath.item-1]<br>            return cell<br><br>        case .AllParcels:<br>            cell = cell as! AllParcelsCell<br>            return cell<br>        }<br><br>    case .AccountSettings:<br>    //etc..Code language: HTML, XML (xml)

Error handling

Whenever we instantiate our enum, we run the risk of the initializer failing and returning nil if we pass it a rawValue that doesn’t correspond to any of its possible values:

let section = Section(rawValue: -42)Code language: JavaScript (javascript)

As you’ve seen, I personally favor a combination of guard statements and assertions to manage error handling for the usual reasons. The guard saves you from wrapping or indenting your “happy” code in an if block and forces you to handle errors. The assertions will cause runtime crashes (just in debug builds, the good kind of crash) and alert you to errors as soon as they appear.

Below are some alternatives:

Force unwrap

let section = Section(rawValue: indexPath.section)!<br><br>    switch section {<br><br>    case .UserDetails:<br><br>    //etc...Code language: HTML, XML (xml)

You’re promising the compiler that there is no way indexPath.section will not correspond to one of the possible values of Section. Break that promise, and you’ll get a runtime crash.

Conditional Binding

if let section = Section(rawValue: indexPath.section) {<br><br>    switch section {<br><br>    case .UserDetails:<br><br>    //etc...Code language: HTML, XML (xml)

Favor Decoupling

Some approaches favor endowing your enum with responsibilities that go beyond basic layout management of your collection view. For example:

enum Section : Int {<br>    case Dough<br>    case Ingredients<br><br>    func title() -> String? {<br>        switch self {<br>        case .Dough: return NSLocalizedString("section_title_dough", comment: "")<br>        case .Ingredients: return NSLocalizedString("section_title_ingredients", comment: "")<br>        }<br>    }<br>}Code language: PHP (php)

I don’t recommend this, as you’re breaking loose coupling by providing data for the collection view, which should be the responsibility of your model and controller classes. It might be a negligible amount of data in this example, but your model may grow more complex and that will unnecessarily increase the enum’s complexity too.

In closing, your enum can talk to your model and controller, but should only be responsible for helping you manage the layout structure of your collection view.

Sample code

You can find all of the above code here.

Acknowledgements

  • Project A Ventures, who gave my team and me a nice project to work on, as well as the time to write a blog post about one of our learnings.
  • Erik Collinder, the very talented designer whose designs are the skin of all the above, and who also gave me the demo screenshots used in this post.
  • Alejandro Santander, a solid indie iOS Developer buddy, for reviewing early drafts of this post.
  • The friendly and knowledgeable people from `#swift`, over at the iOS Developers Slack Community.

References and Further Reading

Implementations

Swift
Objective-C
Documentation
  • Enumerations. iOS Developer Library. The Swift Programming Language (Swift 2.1).
  • The RawRepresentable protocol (conformed to by Swift enums). Swift Standard Library Reference. RawRepresentable Protocol Reference.