Tutorial - Creating Programmatic UICollectionView in Swift

Creating a UICollectionView could be stressful - As compared to dealing with UITableView. But things don't have to be this way for so long. Every time I have to add a CollectionView in the app, I end up spending long time trying to figure out what did I do last time or how to make it work without crashing the app on first try.

I am writing this blog post for future myself, as well as for anyone who might have encountered similar problem. Creating a scaffold code for creating UICollectionView will give the opportunity for easier reference as well as just copy-pasting if you're going to use it for demo/tutorial/coding challenges.

All right! Let's get started. We're going step by step starting from scratch to see how to add and implement simple collectionView with cells to the Swift iOS application.

  1. Adding UICollectionView to the view

First step in implementing full scale UICollectionView is to create an instance of it, add it on the view and set up desired constraints - in this case pinning to all sides of its superview.


private let collectionView: UICollectionView = {
    let viewLayout = UICollectionViewFlowLayout()
    let collectionView = UICollectionView(frame: .zero, collectionViewLayout: viewLayout)
    collectionView.backgroundColor = .white
    return collectionView
}()

override func viewDidLoad() {
    super.viewDidLoad()
    view.backgroundColor = .white
    view.addSubview(collectionView)

    collectionView.translatesAutoresizingMaskIntoConstraints = false

    // Layout constraints for `collectionView`
    NSLayoutConstraint.activate([
        collectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
        collectionView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
        collectionView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor),
        collectionView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor)
    ])
}

A blank UICollectionView with white background - Unfortunately nothing is visible yet!

No surprise it looks like a blank screen. It's ok. We're yet to add necessary designs and collection view cells to it.

2. Setting up delegate, datasource and registering with cell classes

Next thing we want to do with collection view it setting up required datasource and adding some design elements to make it look more presentable. Please note that this step does not modify the UI in any way. We still need the cell and data to be able to display the content

override func viewDidLoad() {
    super.viewDidLoad()
    setupViews()
    setupLayouts()
}

private func setupViews() {
    view.backgroundColor = .white
    view.addSubview(collectionView)

    collectionView.dataSource = self
    collectionView.delegate = self
    collectionView.register(ProfileCell.self, forCellWithReuseIdentifier: ProfileCell.identifier)
}

private func setupLayouts() {
    collectionView.translatesAutoresizingMaskIntoConstraints = false

    // Layout constraints for `collectionView`
    NSLayoutConstraint.activate([
        collectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
        collectionView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
        collectionView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor),
        collectionView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor)
    ])
}

extension ViewController: UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 0
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        return UICollectionViewCell(frame: .zero)
    }

}

extension ViewController: UICollectionViewDelegate {
    
}


We will also create a ProfileCell which is a subclass of UICollectionViewCell.

protocol ReusableView: AnyObject {
    static var identifier: String { get }
}

final class ProfileCell: UICollectionViewCell {
    
}

extension ProfileCell: ReusableView {
    static var identifier: String {
        return String(describing: self)
    }
}

For the sake of simplicity, the we are going to skip the layout code, but will still be available in the Github repository.

3. Populating data

Next part, we are going to populate the data and utilize it to populate UICollectionView data source.


class ViewController: UIViewController {
    ...
      
    ...
    
    var profiles: [Profile] = []
    
    ...
    
    override func viewDidLoad() {
        super.viewDidLoad() 
        ...
        populateProfiles()
        ...
        collectionView.reloadData()
    }
    
    private func populateProfiles() {
        profiles = [
            Profile(name: "Thor", location: "Boston", imageName: "astronomy", profession: "astronomy"),
            Profile(name: "Mike", location: "Albequerque", imageName: "basketball", profession: "basketball"),
            Profile(name: "Walter White", location: "New Mexico", imageName: "chemistry", profession: "chemistry"),
            Profile(name: "Sam Brothers", location: "California", imageName: "geography", profession: "geography"),
            Profile(name: "Chopin", location: "Norway", imageName: "geometry", profession: "geometry"),
            Profile(name: "Castles", location: "UK", imageName: "history", profession: "history"),
            Profile(name: "Dr. Johnson", location: "Australia", imageName: "microscope", profession: "microscope"),
            Profile(name: "Tom Hanks", location: "Bel Air", imageName: "theater", profession: "theater"),
            Profile(name: "Roger Federer", location: "Switzerland", imageName: "trophy", profession: "trophy"),
            Profile(name: "Elon Musk", location: "San Francisco", imageName: "graduate", profession: "graduate")
        ]
    }

}

4. UICollectionViewCell layout and subviews

Just having profile data is not enough, but we will also need to add subviews to our collection view cell subclass and pass the Profile object to populate cell subviews with necessary information.

final class ProfileCell: UICollectionViewCell {
    private let profileImageView: UIImageView = {
        let imageView = UIImageView(frame: .zero)
        imageView.contentMode = .scaleAspectFill
        return imageView
    }()

    let name: UILabel = {
        let label = UILabel(frame: .zero)
        label.textAlignment = .center
        return label
    }()

    let locationLabel: UILabel = {
        let label = UILabel(frame: .zero)
        label.textAlignment = .center
        return label
    }()

    let professionLabel: UILabel = {
        let label = UILabel(frame: .zero)
        label.textAlignment = .center
        return label
    }()
    
    // The code to set up views and laying out constraints has been immitted for clarity and brevity
    
    // A method to pass model so that it can be applied to cell subview elements
    
    func setup(with profile: Profile) {
        profileImageView.image = UIImage(named: profile.name)
        name.text = profile.name
        locationLabel.text = profile.location
        professionLabel.text = profile.profession
    }

}

5. Updating UICollectionView data source to consume the data and pass model over to collection view cell

Next part is to update the collection view data source methods so that we can pass this data directly to collection view cells.

extension ViewController: UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return profiles.count
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ProfileCell.identifier, for: indexPath) as! ProfileCell

        let profile = profiles[indexPath.row]
        cell.setup(with: profile)
        return cell
    }
}

6. Setting up collection view layout and returning size of individual items

However, we're not done yet. Collection view requires us to return the size of every cell. Which either needs to be hardcoded or calculated for each cell based on the content.

To do this, we will have our viewController class conform to UICollectionViewDelegateFlowLayout and implement following method,

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize

First we will add necessary layout constants to specify individual cell height,

private enum LayoutConstant {
    static let spacing: CGFloat = 16.0
    static let itemHeight: CGFloat = 300.0
}

Then we will implement above-mentioned method to return the item size. Please note that we are calculating cell width based on number of items in a single row,


extension ViewController: UICollectionViewDelegateFlowLayout {
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {

        let width = itemWidth(for: view.frame.width, spacing: 0)

        return CGSize(width: width, height: LayoutConstant.itemHeight)
    }

    func itemWidth(for width: CGFloat, spacing: CGFloat) -> CGFloat {
        let itemsInRow: CGFloat = 2

        let totalSpacing: CGFloat = 2 * spacing + (itemsInRow - 1) * spacing
        let finalWidth = (width - totalSpacing) / itemsInRow

        return finalWidth - 5.0
    }
}

In portrait mode,

However, this does not look so good. If we change the background color of content view of each cell, we can see they are stacked right next to the superview with inconsistent padding.

Let's try to make it look bit nicer by implementing additional methods in UICollectionViewDelegateFlowLayout protocol,

7. Adding Spacing and collectionView section insets


extension ViewController: UICollectionViewDelegateFlowLayout {
    ....
    
    ....
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
        return UIEdgeInsets(top: LayoutConstant.spacing, left: LayoutConstant.spacing, bottom: LayoutConstant.spacing, right: LayoutConstant.spacing)
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
        return LayoutConstant.spacing
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
        return LayoutConstant.spacing
    }
    
    ....
    
    ....
}

And after all this hard work and code, we finally get the layout which looks like this,

  1. With all cells with populated data
  2. Consistent spacing and padding between cells and sections
  3. Ability to control number of items in a row thereby automatically adjusting any surrounding spacing

In case we want to display 3 cells per row, we can simply update the parameter itemsInRow in method func itemWidth(for width: CGFloat.

And that's all for creating a basic collection view. Some tips on things you might need to change to make it fit your requirements are as follows,

  1. UICollectionViewCell subclass
  2. Cell model
  3. Layout, padding and spacing around cells
  4. number of items per row
  5. Ability to handle change in layout and number of items in response to orientation change
If you have further questions on usage or advanced options, feel free to reach out to me directly through comments section or Twitter
The full source code of this tutorial is available on GitHub under UICollectionViewDemo. Looking forward to your comments and feedback