In one of the Android apps I work on, I display content in a list. So far nothing fancy. Except that it is a list of cards. In Android I can simply use a CardView for this. It looks beautiful to have these cool cards with their elevation effect. But when I tried to implement a similar layout in iOS, I encountered some problems which I want to talk about in this post.

So here is the layout from the Android app. Quiet simple, isn’t it? In Android this kind of layout is achieved using a RecyclerView where each item is a CardView. Did I ever mention that I really like how easy you can create layouts in Android? The whole XML system has some big advantages over iOS AutoLayout if you ask me. But we are not here to complain about whats worse in one or the other OS, are we?

My first intuition to recreate this on iOS was to use a UICollectionView where each item would be a card. I then used the technique I described here to create the self-sizing of the items. Then I added some layer magic to the UICollectionViewCell to add a drop shadow and I was good to go. The solution worked so why were I not happy? Well because you might be familiar with what they say: the best code is the code you don’t write.

Using a UICollectionView forced me to write a custom layout class, use a sizing cell and do all the measurements myself. Also my client wanted only one row so why did I even bother using a UICollectionView in the first place when there is a perfect UI component for lists: UITableView? The answer is: because I thought it was better to have multiple columns so I developed the UI so you could easily have more than one column by flipping a switch in the settings.

After some discussions, the client made the decision that there will never be more than one column so I thought now would be a good time to clean up the code and do some refactoring. A good chance to reduce and simplify the amount of code which could possibly contain errors.

First I started by replacing the UICollectionView with a UITableView and removing the custom layout completely. The next step was the custom UITableViewCell. Here is what it looks like in the xib:

The trick here is to add your UI not directly to the Content View of the UITableViewCell but add another container which you can use as your “card”. I’m not 100% sure if I couldn’t simply have used the StackView but I went with a plain UIView as my “Container View”. The StackView is pinned to top, bottom, left and right of the Container View (respecting the default margin). The Container View is pinned to top, bottom, left and right of the Content View without any margins. This is because I later will set the margin in code.

The next step was to implement the custom code for the cell. Heres what it looks like:

class StatusTableViewCell: UITableViewCell {

    @IBOutlet weak var containerView: UIView!
    @IBOutlet weak var stackView: UIStackView!
    @IBOutlet weak var iconImageView: UIImageView!
    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var dateLabel: UILabel!
    @IBOutlet weak var descriptionLabel: UILabel!
    @IBOutlet weak var statusValueLabel: UILabel!
    @IBOutlet weak var levelImageView: UIImageView!
    @IBOutlet weak var alertCountLabel: BadgeView!
    
    @IBOutlet weak var levelImageWidthConstraint: NSLayoutConstraint!
    @IBOutlet weak var iconImageWidthConstraint: NSLayoutConstraint!
    
    @IBOutlet weak var containerViewTopConstraint: NSLayoutConstraint!
    @IBOutlet weak var containerViewRightConstraint: NSLayoutConstraint!
    @IBOutlet weak var containerViewBottomConstraint: NSLayoutConstraint!
    @IBOutlet weak var containerViewLeftConstraint: NSLayoutConstraint!
    
    override func awakeFromNib() {
        super.awakeFromNib()
        alertCountLabel.badgeColor = .batteryRed
        alertCountLabel.textColor = .white
        levelImageWidthConstraint.constant = CGFloat(AppConfig.levelIconSize)
        iconImageWidthConstraint.constant = CGFloat(AppConfig.iconSize)
                
        prepareForReuse()
    }
    
    override func prepareForReuse() {
        super.prepareForReuse()
        iconImageView.image = nil
        titleLabel.text = nil
        dateLabel.text = nil
        descriptionLabel.text = nil
        statusValueLabel.text = nil
        levelImageView.image = nil
        alertCountLabel.text = nil
    }
    
    override func draw(_ rect: CGRect) {
        super.draw(rect)
        //Set containerView drop shadow
        if AppConfig.displayStatesAsCards {
            containerView.layer.borderWidth = 1.0
            containerView.layer.borderColor = UIColor.white.cgColor
            containerView.layer.shadowColor = UIColor.lightGray.cgColor
            containerView.layer.shadowRadius = 2.0
            containerView.layer.shadowOpacity = 1.0
            containerView.layer.shadowOffset = CGSize(width:0, height: 2)
            containerView.layer.shadowPath = UIBezierPath(rect: containerView.bounds).cgPath
        }
    }
    
}

As you see, I awakeFromNib I do some styling for some of the components depending on the current settings. The more interesting part is the draw function. Here I add the drop shadow to the containerView via it’s layer. This allows for smooth scrolling. Thats it for the cell. If you would just run the code now, you wouldn’t see any cards though but just a normal UITableView.

The trick to display the entries as cards is now to set the constants of the constraints. This is done in the UIViewController which contains the UITableView. To get the self sizing cells, we have to activate automatic dimensions first. There are two good places to do this. In viewDidLoad() or as I prefer, in the didSet of the tableView property. Since I always use plain UIViewController over UITableViewController, I add a outlet to my UITableView and do the setup there like this:

    @IBOutlet weak var tableView: UITableView! {
        didSet {
            //Register cell
            tableView.registerCellWithNib(StatusTableViewCell.self)
            
            //Setup pull to refresh
            tableView.refreshControl = UIRefreshControl()
            tableView.refreshControl?.addTarget(self, action: #selector(reloadDataSource), for: .valueChanged)
        
            //Configure TableView
            tableView.rowHeight = UITableViewAutomaticDimension
            tableView.estimatedRowHeight = 44.0
            tableView.separatorStyle = .none
        }
    }

The registerCellWithNib is a extension which works in conjunction with a protocol and a default implementation. Maybe I’ll write another article on this in the future (I got the idea from here). Basically it allows you to easily register and dequeue cells without the need to have strings as reuse identifiers. Anyways, the interesting part is to set the rowHeight of the tableView to automatic dimension and give the tableView a estimatedRowHeight to work with. Also set the separator style to none.

Thats it. Now the UITableView will automatically calculate the correct height for each row based on the auto layout constraints you setup. But that still doesn’t give you a card like layout. For this, we need to do the final step which is setting up the margin for the Container View. We will do this in cellForRowAt:indexPath: like so:

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(for: indexPath) as StatusTableViewCell
        
        let viewModel = diffCalc.rows[indexPath.row]
        cell.titleLabel.text = viewModel.title
        cell.dateLabel.text = viewModel.date
        cell.descriptionLabel.text = viewModel.description
        cell.iconImageView.image = viewModel.iconImage
        cell.iconImageView.tintColor = viewModel.iconColor
        cell.statusValueLabel.text = viewModel.statusValue
        cell.alertCountLabel.text = viewModel.alertCount
        cell.alertCountLabel.isHidden = viewModel.alertCount == nil
        cell.levelImageView.image = viewModel.levelImage
        cell.levelImageView.tintColor = viewModel.levelColor
        cell.selectionStyle = viewModel.selectionStyle
        
        if AppConfig.displayStatesAsCards {
            //Disable cell clipping
            cell.clipsToBounds = false
            
            //Set containerView padding
            cell.containerViewTopConstraint.constant = indexPath.row == 0 ? 8.0 : 4.0
            cell.containerViewBottomConstraint.constant = indexPath.row == diffCalc.rows.count - 1 ? 8.0 : 4.0
            cell.containerViewRightConstraint.constant = 8.0
            cell.containerViewLeftConstraint.constant = 8.0
            
            //Make cell selection invisible
            cell.selectionStyle = .none
            
        } else {
            cell.containerViewTopConstraint.constant = 0
            cell.containerViewBottomConstraint.constant = 0
            cell.containerViewRightConstraint.constant = 0
            cell.containerViewLeftConstraint.constant = 0
        }
        
        return cell
    }

The interesting part here is setting the cell.containerViewXXXConstraint based on the row. I wanted a 8px space between each “card” so that meant that I couldn’t simply add 8px top and bottom constraint or it would result in a 16px margin between each row. Instead, I check if the current indexPath.row is the first or the last row and only in this case add a 8px margin. Otherwise I add 4px margins to get the 8px in total.

The final result looks like this:

So the result looks exactly the same as it did with the UICollectionView and I also have self-sizing cells but the amount of code was reduced drastically. The collection view layout class was removed completely as well as the sizing cell. I didn’t had to do any sizing of my own anymore and all I had to do was setup correct layout constraints on my UITableViewCell.

I saw a post on StackOverflow the other day where somebody asked how he could get a self-sizing single column UICollectionView with full width items. Here you have it. If you only want one column, don’t bother using UICollectionView. Though the latest version comes with options for fully automatic sizing of items but if you need full width items, you are better off using a good old UITableView. It is very well designed and versatile in it’s layout. You can do a lot of things with it.

Don’t get fooled by thinking you need the more complex UICollectionView. In a lot of scenarios you can get away with the simpler tableview, saving you code, time, headache and even make your app faster. Automatic sizing of tableviews is done by Apple and they (most of the time) know what they do. In my tests, the self-sizing tableview was a lot faster than my self-sizing collectionview.

I hope this article helps you create cool card layouts and improve your code. Cheers ✌️