Creating a reusable UITableViewCell

in #programming7 years ago (edited)

When we code, we always try not to repeat ourselves. If we have multiple implementations of the same functionality it will cause problems for us somewhere down the line. For this reason, we would usually reuse a component, when we see the potential for repeating the same code. But are we doing it in the correct way?

I would like to show this problem on a simple example, based on what I have encountered in practice. Imagine a UITableView cell implementation of a cell that is being used more than once in the app. In my example, the cell looks the same, but it is displaying different data. It also might be showing or hiding some components

The problem

Writing extendable code.png
(State showing the image and 4 labels)

Writing extendable code (1).png
(State showing only two of the labels)

We have title and subtitles on the left and right side and an image view. We will show the same cell in many screens in the app. Sometimes we have to hide some of the components and we have to populate this cell with different data on every screen. This cell must display the user data on one screen and the data for a pet in another screen.

So we create one cell in order to reuse it everywhere we need it. We create a view in the interface builder and connect it to the swift class: ReusableTableViewCell.

Screen Shot 2018-02-06 at 22.12.08.png

Here we have two models, User and Pet that we want to display in this cell. Since the user doesn't have a picture, we should hide the image view when displaying a user.

struct User {
    let name: String
    let nickname: String
}

struct Pet {
    let name: String
    let owner: String
    let breed: String
    let weight: Int
    let avatar: UIImage?
}

One way of implementing this would be to add two functions to the ReusableTableViewCell, set(with user: User) and set(with pet: Pet). In those two functions, we would set the labels and toggle the visibility of the image view. We would hide the image view when displaying the user data and show the image view when displaying the pet data.

func set(with user: User) {
    leftTitleLabel.text = user.name
    leftSubtitleLabel.text = "Nick: " + user.nickname
    hideImage()
}

func set(with pet: Pet) {
    leftTitleLabel.text = pet.name
    leftSubtitleLabel.text = "Owner: " + pet.owner
    
    rightTitleLabel.text = pet.breed
    rightSubtitleLabel.text = pet.weight.description
    
    iconImageView.image = pet.avatar
    showImage()
}

private func showImage() {
    iconWidthConstraint.constant = 50
    iconLeftConstraint.constant = 8
}

private func hideImage() {
    iconWidthConstraint.constant = 0
    iconLeftConstraint.constant = 0
}

I would argue that this is not the best way to go about doing something like that. I would like to reference the open-closed principle that says that classes should be:

“open for extension, but closed for modification.”

The principle originated with Bertrand Meyer who wrote about it in his book Object-Oriented Software Construction. You can also read more about it in an article from Robert C. Martin, The Open-Closed Principle.

A Different Approach

Our goal is to write the ReusableTableViewCell once, but leave it open for extension for all the existing cases and all the cases that may come up in the future. So what do we gain from this? We won’t ever have to change the code in the ReusableTableViewCell itself or at least we won’t have to change it that often. This will help a lot if we have many places in our app that are using this cell. Everytime we are changing the ReusableTableViewCell, we can introduce a bug that can affect a large part of our app. We also would have to consider every case where this cell is used and try not to break that specific case. This can introduce bugs that are hard to discover.

If the cell will be able to show a lot more models in the future it will get bloated since we will have to write a function for every model it will be able to display. The cell itself shouldn't be aware of all those classes and how they must be presented.

So how do we achive this?

We will introduce a new protocol called ReusableTableViewCellDataProvider. This will be the data source that the table view cell will get the data it needs from. This protocol tells us what the cell can display and that's the only thing this cell will be aware of.

protocol ReusableTableViewCellDataProvider {
    var leftTitle: String? { get }
    var leftSubtitle: String? { get }
    var rightTitle: String? { get }
    var rightSubtitle: String? { get }
    var image: UIImage? { get }
}

We will than add a UserReusableCellDataProvider that will implement the ReusableTableViewCellDataProvider protocol. It will act as an adapter between the ReusableTableViewCell and the User model. It is the job of the UserReusableCellDataProvider to provide the data that the cell can display.

struct UserReusableCellDataProvider: ReusableTableViewCellDataProvider {
    let user: User

    var leftTitle: String? {
        return user.name
    }

    var leftSubtitle: String? {
        return "Nick: " + user.nickname
    }

    var rightTitle: String? {
        return ""
    }

    var rightSubtitle: String? {
        return ""
    }

    var image: UIImage? {
        return nil
    }
}

In the cell itself, we can remove the functions that accept concrete classes for User and Pet since we only need to pass in any object that implements the ReusableTableViewCellDataProvider protocol.

func set(with dataProvider: ReusableTableViewCellDataProvider) {
    leftTitleLabel.text = dataProvider.leftTitle
    leftSubtitleLabel.text = dataProvider.leftSubtitle
    
    rightTitleLabel.text = dataProvider.rightTitle
    rightSubtitleLabel.text = dataProvider.rightSubtitle
    
    set(image: dataProvider.image)
}

With set(image: UIImage) function we make sure that we hide the imageView if the data provider doesn't return an image

private func set(image: UIImage?) {
    guard let image = image else {
        hideImage()
        return
    }
    
    showImage()
    iconImageView.image = image
}

When dequeuing the cell in tableView cellForRowAt indexPath function of the users screen, we will create an instance of the UserReusableCellDataProvider and pass it into the ReusableTableViewCell.

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    guard let cell = tableView.dequeueReusableCell(withIdentifier: "ReusableTableViewCell") as? ReusableTableViewCell, indexPath.row < users.count  else {
        return UITableViewCell()
    }
    
    let user = users[indexPath.row]
    let dataProvider = UserReusableCellDataProvider(user: user)
    cell.set(with: dataProvider)
    return cell
}

If we want to display the Pet data in the same cell, we only need to create a new DataProvider.

struct PetReusableCellDataProvider: ReusableTableViewCellDataProvider {
    let pet: Pet

    var leftTitle: String? {
        return pet.name
    }

    var leftSubtitle: String? {
        return "Owner: " + pet.owner
    }

    var rightTitle: String? {
        return pet.breed
    }

    var rightSubtitle: String? {
        return pet.weight.description
    }

    var image: UIImage? {
        return pet.avatar
    }
}

In the screen with the list of pets, we will deque the cell in the same way, but pass in the PetReusableCellDataProvider.

The end result

Screen Shot 2018-02-07 at 00.04.45.png

Pets


Screen Shot 2018-02-07 at 00.04.31.png

Users

As you can see, we have created two ways of displaying data in our cell but we didn’t have to touch any of the code in the cell itself. If we wanted, we could also add new DataProviders without bloating the cell.

You can find the example project on GitHub

Sort:  

Congratulations @damage-un! You received a personal award!

Happy Birthday! - You are on the Steem blockchain for 1 year!

Click here to view your Board

Support SteemitBoard's project! Vote for its witness and get one more award!

Congratulations @damage-un! You received a personal award!

Happy Birthday! - You are on the Steem blockchain for 2 years!

You can view your badges on your Steem Board and compare to others on the Steem Ranking

Vote for @Steemitboard as a witness to get one more award and increased upvotes!