A different approach to UITableView delegate methods: a cool use of Swift's enums.

A different approach to UITableView delegate methods: a cool use of Swift's enums.

I love enums, for me they’re the coding equivalent to strumming a ukulele, eating pizza, and all the other things I love. But all joking aside, I’ve had a deep respect for this tool since I was coding small homebrew demos in C for the Gameboy Advance handheld console. Back then, they were a great tool that allowed us to have cleaner code and avoid issues when using constant values by assigning names to them. Since then, Apple has improved enumerators in Swift, by giving us the chance to store values (great for creating our own ADTs - Algebraic Data Types) and to include methods within them. These factors combined with the power of Swift’s pattern matching gives us an expansive field to explore and create new patterns for use in our apps.

One method that we’ve recently experimented with was using enums to describe data needed by UITableViews. As you’re probably aware, when we need to display table views in our apps, which is the majority of the time, we implement the UITableViewDelegate and UITableViewDataSource protocols. Using these with our tables tells the app what data to display and how to draw it. Delegate patterns are useful and have deep foundations set on the iOS SDK, but personally, I’ve never been happy with how the information and the code itself are organized inside this method. So, why not try to use the new all-powerful Swift enums to tidy everything up?

We’ve set up an approach on how to better organize code to create a table view containing several cell types. In our example, the table will hold the information about pizza (dough type and thickness, sauce type, and ingredients), that will be represented by controls like sliders, picker views and switches. This type of table would have complex code in the delegate methods to distinguish between the different cell types, where to obtain the data to draw them, and how their changes affect our model. We’re going to move all those methods to enums that depict our data.

All the code examples in this post come from a project we’ve set up in this open repository. Don’t hesitate to check it out!

Structuring our enums

We’ll represent each set of section rows as an enum containing several methods that we’ll call from our delegate methods. We’ll have another enum represent the entire set of sections themselves. We need two sections in our app, one to set our pizza’s dough settings and another to choose its ingredients. We’re even giving our users the ability to choose bacon as a topping! We wouldn’t want to live in a world with bacon-less pizzas, but maybe this is the wrong time to think about that scenario. Instead, let’s list the structure of both of our table sections like this:

Bacon is set on by default, as we think it should be :D

enum Section : Int {
    case Dough = 0
    case Ingredients = 1

    static let «allValues» = [Section.Dough, Section.Ingredients]

    func «title»() -> String? {
        switch self {
        case .Dough: return NSLocalizedString("section_title_dough", comment: "")
        case .Ingredients: return NSLocalizedString("section_title_ingredients", comment: "")
        }
    }

    func «caseForRow»(row: Int) -> Row? {
        switch self {
        case .Dough: return SectionDoughRows(rawValue: row)
        case .Ingredients: return SectionIngredientsRows(rawValue: row)
        }
    }

    func headerHeight() -> CGFloat {
        return CGFloat(kTableCellHeightRegular)
    }

    func headerView(tableView: UITableView) -> UIView? {
        let header = tableView.dequeueReusableHeaderFooterViewWithIdentifier(kTableHeaderIdentifier) as! TableHeaderView
        if let sectionName = self.title() {
            header.lblTitle.text = sectionName
        }
        return header
    }
}
»allValues|It's useful to have a constant array like this in our enums, to allow us to get the number of enum cases inside our enum.«
»title|Simply enough, we'll access our section titles by calling this method.«
»caseForRow|This method will allow us to access each row's enum based on a certain row as you'll see in a few moments.«

For each section, we need a title, number of rows, and each row’s enum representations. That’s what we have here. Our Section enum holds its internal cases in an array we can use to count them (Swift’s enum can’t count its cases yet), and also implements two methods so we can ask it for its title and each section’s row enums. Less relevant are the last two methods, that help our table know how to draw each section’s header. As you’ve probably noticed, we have two new types here, SectionDoughRows and SectionIngredientsRows, that will hold our data for each row in each section. The problem is that Swift is so strictly typed that each enum will be treated as a different type even if they contain the same group of methods. To fix this, we need to create a protocol which every enum representing a row will need to conform to Row. Take a look at it:

protocol Row {
    func «title»() -> String
    func «configurationFunctionAndIdentifier»(owner: MainViewController) -> (((UITableViewCell, NSIndexPath) -> UITableViewCell), String)
    func «rowHeight»() -> CGFloat
    func «currentValue»(owner: MainViewController) -> Any?
    func «assignValue»(owner: MainViewController, value: Any?)
}
»title|This method will represent each row's title, if they have one.«
»configurationFunctionAndIdentifier|This method returns two values in a tuple, a cell identifier so we can dequeue our cell and a function we can call to configure it.«
»rowHeight|This method will return the height for an specific cell row.«
»currentValue|This method retrieves the value held by an speficic cell from an <code>owner</code> (in our case, the view controller).«
»assignValue|This method will modify the value held by the <code>owner</code> of the model (again, the view controller) corresponding to an specific cell; after an user interaction.«

As you can see, if every row’s enum conforms to this protocol, we’ll have everything our table needs to know how to draw itself. This will also allow us to respond to our user’s interaction to modify our model. Let’s see how to conform to this protocol by looking at the first of our rows, the one that represents the pizza’s dough:

enum SectionDoughRows : Int, Row {
    case Thickness = 0
    case CheeseBorder = 1

    static let allValues = [SectionDoughRows.Thickness, SectionDoughRows.CheeseBorder]

    func title() -> String {
        switch self {
        case .Thickness: return NSLocalizedString("row_dough_thickness_title", comment: "")
        case .CheeseBorder: return NSLocalizedString("row_dough_cheese_border_title", comment: "")
        }
    }

    func «configurationFunctionAndIdentifier»(owner: MainViewController) -> (((UITableViewCell, NSIndexPath) -> UITableViewCell), String) {
        switch self {
        case .Thickness: return (owner.configureSliderCell, kTableCellIdentifierSlider)
        case .CheeseBorder: return (owner.configureSwitchCell, kTableCellIdentifierSwitch)
        }
    }

    func rowHeight() -> CGFloat {
        return CGFloat(kTableCellHeightRegular)
    }

    func currentValue(owner: MainViewController) -> Any? {
        switch self {
        case .Thickness: return owner.doughThickness
        case .CheeseBorder: return owner.shouldHaveCheeseBorder
        }
    }

    func «assignValue»(owner: MainViewController, value: Any?) {
        switch self {
        case .Thickness:
            if let thickness = value as? Float {
                owner.doughThickness = DoughThickness.thicknessFromSliderValue(thickness)
            }
        case .CheeseBorder:
            if let shouldHaveCheeseBorder = value as? Bool {
                owner.shouldHaveCheeseBorder = shouldHaveCheeseBorder
            }
        }
    }
}
»configurationFunctionAndIdentifier|Most of these methods are self-explanatory, but maybe this is the most complex. When we need to draw a cell we need its cell identifier to dequeue it, and also a configuration method that knows how to draw it. Here, our method will receive the owner of the configuration functions (in our case <code>MainViewController</code>) to return the cell identifier and configuration function for an specific cell type in one swift strike.«
»assignValue|This method also access our main view controller, but in this case it allows us to depict how we want our user's interaction with the cell's controls will affect the model inside it.«

Most of these methods are fairly self-explanatory, they simply redirect our table’s delegate methods to their much-craved data: their title, cell identifiers, functions to configure their visuals, value to represent. The last method (assignValue) is a little bit more complex. It needs to connect the user’s interaction with the controls inside our rows (i.e.: moving a slider to select a new dough thickness) to changes in our view controller’s model. Notice our use of optional casting to handle several possible types of values (thickness is represented as a Float, but the chance of cheeseborderness is depicted using Bool).

Our thinner delegate methods

So how do our table’s delegate methods look? Thinner! In fact, now they’re mostly proxies to our enum methods:

func numberOfSectionsInTableView(tableView: UITableView) -> Int {
    return Section.allValues.count
}

func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    let sectionCase = Section.allValues[section]
    switch sectionCase {
    case .Dough: return SectionDoughRows.allValues.count
    case .Ingredients: return SectionIngredientsRows.allValues.count
    }
}

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let sectionCase = Section.allValues[indexPath.section]
    if let rowCase = sectionCase.«caseForRow»(indexPath.row) {
            let (configFunction, identifier) = rowCase.configurationFunctionAndIdentifier(self)
            return «configureCellForIdentifier»(tableView, cellIdentifier: identifier, indexPath: indexPath, configurationFunction: configFunction)
    }
    return UITableViewCell(style: .Default, reuseIdentifier: nil)        
}
»caseForRow|We use the <code>caseForRow</code> to access each row's enum from its section. By using optionals we know we're perfectly safe regarding types.«
»configureCellForIdentifier|These two lines allows us to obtain the cell identifier and configuration function for each cell type, and later dequeue it and customize it.«

Again, nothing complicated. When our table asks our view controller for the number of sections available, the number of rows of one of them, or information to draw a cell, we just call the methods in our enums. The only thing that may need a little bit of attention is the way we get each row’s enum case, by using the caseForRow method. You may notice that we’re not paying attention to the types here, and we don’t need to. We have the Row protocol we can rely on to access the methods we need, and by using optional unwrapping we know that we’ll always get a valid row result no matter what happens.

Now we should take a look at how we’re going to configure our cells:

func configureCellForIdentifier(tableView: UITableView, cellIdentifier: String, indexPath: NSIndexPath, configurationFunction: (UITableViewCell, NSIndexPath) -> UITableViewCell) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as! UITableViewCell
    return «configurationFunction»(cell, indexPath)
}

func configureSliderCell(cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let sectionCase = Section.allValues[indexPath.section]
    if let sliderCell = cell as? SliderTableViewCell,
        let rowCase = sectionCase.«caseForRow»(indexPath.row) {
            sliderCell.slider.addTarget(self, action: "didChangeSliderValue:", forControlEvents: .ValueChanged)
            sliderCell.lblTitle.text = rowCase.title()
            sliderCell.slider.«indexPath» = indexPath

            if let value = rowCase.currentValue(self) as? DoughThickness {
                sliderCell.lblValue.text = "\(value.title())"
                sliderCell.slider.value = value.floatValue()
            }
        return sliderCell
    }
    return cell
}
»configurationFunction|Here we just call the configuration function just obtained from the enum case representing our row.«
»Row access|This pattern is repeated through our code, we access sections and rows the same way all the time regardless of their type.«
»indexPath|One problem related with using views inside cells, is that these views' delegate methods only give us access to their instances and not the cells they live in. We've set up an indexPath property in an UIView extension to bypass this issue.«

Our configureCellForIdentifier will dequeue our cell and call the corresponding configuration function to customize each kind of cell. One example of these functions is configureSliderCell that sets title, value, and our reference as the delegate for the dough thickness slider cell. Lastly, let’s see how to assign values from our cell’s interactions to our model:

func assignValueForRowAtIndexPath(someIndexPath: NSIndexPath?, value: Any?) {
    if let indexPath = someIndexPath,
        let sectionCase = Section(rawValue: indexPath.section),
        let rowCase = sectionCase.caseForRow(indexPath.row) {
            rowCase.assignValue(self, value: value)
            «tblMain.reloadRowsAtIndexPaths»([indexPath], withRowAnimation: UITableViewRowAnimation.None)
    }
}

// UISlider change value event:    
func didChangeSliderValue(slider: UISlider) {
    assignValueForRowAtIndexPath(slider.indexPath, value: slider.value)
}
»reloadRowsAtIndexPaths|To show every value change in our cell we need to reload. This can be risky when using views that have a lot of animations of possible different values... use wisely!«

Here we have a common method to the functions that assign values from our cell row cases like we had with the cell configuration functions. In this example, we see how the method that handles value change in the slider can call assignValueForRowAtIndexPath, which will then access the corresponding row’s assignValue function as defined in the Row protocol.

Conclusion

There’s a bit more code involved here, but this example covers the basics of our approach to the problem. Our goal is to have all of our cell’s needs organized by our model data and table organization, not the other way around. Swift’s powerful enumerations, together with optionals and first-class functions, allow us to tidy our tableView delegate methods up in a cleaner and more elegant way. You can find the rest of the code in this repo. As always, if you have questions or comments about this idea, don’t hesitate to let us know through Facebook, Twitter or the comments section below.

And please, don’t switch off the bacon button. You shouldn’t do that to a pizza.

blog comments powered by Disqus

Ensure the success of your project

47 Degrees can work with you to help manage the risks of technology evolution, develop a team of top-tier engaged developers, improve productivity, lower maintenance cost, increase hardware utilization, and improve product quality; all while using the best technologies.