Handle different UICollectionViewCell in same controller

No need for messy if-else statements any more.

I am working on a project has more collection view screens, and each collection view has more different cells, and if you faced same situation you know you will need for more of if-else statements, and this will lead you to messy cellForItem method, I mad some search and find this great article, and here I will show how this article helped me to fix this messy code.

CollectionCellController

The main piece of our patten what we will call it CollectionCellController, and this controller will be as protocol has different functions need to implement, we need a controller for each cell to register cell, dequeue cell, select action for cell, size cell, and more.

import UIKit

protocol CollectionCellController {    
    static func registerCell(on collectionView: UICollectionView)
    func cellFromCollectionView(_ collectionView: UICollectionView, forIndexPath indexPath: IndexPath) -> UICollectionViewCell
    func didSelectCell()
    func sizeForItem(collectionView: UICollectionView) -> CGSize
    func insetForSection(collectionView: UICollectionView) -> UIEdgeInsets
}
extension CollectionCellController {
    func sizeForItem(collectionView: UICollectionView) -> CGSize {
        .zero
    }
    func insetForSection(collectionView: UICollectionView) -> UIEdgeInsets {
        .zero
    }
}

What next:

For each UICollectionViewCell we need two things:

  • Data model hold all data cell needs, and to differentiate between cells.

  • Controller to handle all cell needs.

We here will show model data and collection cell controller for only two cells, please see the full demo in the end of the article.

Data Models:

For each cell, we need to provide a model data hold all data the cell will need:

// ImagePostStore.swift
import UIKit

struct ImagePostStore {
    let image: UIImage
}
// ProfileStore.swift
import Foundation

struct ProfileStore {
    let profile: Profile
}

XCollectionCellController

The CollectionCellController will manage dequeuing, selecting, sizing cell, and more.

import UIKit

class ProfileCollectionCellController: CollectionCellController {

    fileprivate let item: ProfileStore

    init(item: ProfileStore) {
        self.item = item
    }
    fileprivate static var cellIdentifier: String {
        return String(describing: ProfileCollectionViewCell.self)
    }

    static func registerCell(on collectionView: UICollectionView) {
        collectionView.register(UINib(nibName: cellIdentifier, bundle: nil), forCellWithReuseIdentifier: ProfileCollectionViewCell.identifier)
    }

    func cellFromCollectionView(_ collectionView: UICollectionView, forIndexPath indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ProfileCollectionViewCell.identifier, for: indexPath) as! ProfileCollectionViewCell
        return cell
    }

    func didSelectCell() {
    }

    func sizeForItem(collectionView: UICollectionView) -> CGSize {
        CGSize(width: collectionView.frame.width, height: 90)
    }
}
import UIKit

class ImagePostCollectionCellController: CollectionCellController {

    fileprivate let item: ImagePostStore

    init(item: ImagePostStore) {
        self.item = item
    }
    fileprivate static var cellIdentifier: String {
        return String(describing: ImagePostCollectionViewCell.self)
    }

    static func registerCell(on collectionView: UICollectionView) {
        collectionView.register(UINib(nibName: cellIdentifier, bundle: nil), forCellWithReuseIdentifier: ImagePostCollectionViewCell.identifier)
    }

    func cellFromCollectionView(_ collectionView: UICollectionView, forIndexPath indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ImagePostCollectionViewCell.identifier, for: indexPath) as! ImagePostCollectionViewCell
        cell.configure(item.image)
        return cell
    }

    func didSelectCell() {
    }

    func sizeForItem(collectionView: UICollectionView) -> CGSize {
        ImagePostCollectionViewCell.sizeForItem(collectionView: collectionView)
    }

    func insetForSection(collectionView: UICollectionView) -> UIEdgeInsets {
        ImagePostCollectionViewCell.insetForSection(collectionView)
    }
}

CollectionCellControllerFactory

By using factory design pattern, we will can get the appropriate UICollectionViewCell by its data model:

import UIKit

struct CollectionCellFactory {
    func cellControllers(with items: [Any]) -> [CollectionCellController] {
        return items.map { item in
            if let profileStore = item as? ProfileStore {
                return ProfileCollectionCellController(item: profileStore)
            } else if let mainTitleStore = item as? MainTitleStore {
                return MainTitleCollectionCellController(item: mainTitleStore)
            } else if let imagePostStore = item as? ImagePostStore {
                return ImagePostCollectionCellController(item: imagePostStore)
            } else if let recentPhotosStore = item as? RecentPhotosStore {
                return RecentPhotosCollectionCellController(item: recentPhotosStore)
            }
            return MainTitleCollectionCellController(item: item as! MainTitleStore)
        }
    }
}

Collect the pieces

In our controller we will build the collection view by using data model of cells, and then by using CollectionCellControllerFactory we will get the appropriate CollectionCellController .

p.s We assume here our controller has more than one section so we use 2-D array of CollectionCellControllers, so if you are using one section, you will need just 1-D array of CollectionCellController

 //MARK: - Variables
    fileprivate var collectionCellControllers = [[CollectionCellController]]()
    fileprivate var collectionCellFactory = CollectionCellFactory()

    //MARK: - Methods
    fileprivate func buildCollectionView() {
        let recentPhotos = [UIImage(named: "img1")!, UIImage(named: "img2")!, UIImage(named: "img3")!, UIImage(named: "img4")!, UIImage(named: "img5")!, UIImage(named: "img6")!]

      // prepare your cells data models
        let firstSection = [ProfileStore(), MainTitleStore(title: "Recent photos"), RecentPhotosStore(photos: recentPhotos), MainTitleStore(title: "Other photos")] as [Any]

        let secondSection = [ImagePostStore(image: UIImage(named: "img1")!), ImagePostStore(image: UIImage(named: "img2")!), ImagePostStore(image: UIImage(named: "img3")!), ImagePostStore(image: UIImage(named: "img4")!), ImagePostStore(image: UIImage(named: "img5")!), ImagePostStore(image: UIImage(named: "img6")!)]

      // create collection cell controllers
        for section in [firstSection, secondSection] {
            collectionCellControllers.append(collectionCellFactory.cellControllers(with: section))
        }
    }

One single line cellForItem method!

So now we have array of CollectionCellControllers, and we only just fill methods of UICollectionViewDataSource and UICollectionViewDelegateFlowLayout protocols.

extension UserCollectionViewController {
    override func numberOfSections(in collectionView: UICollectionView) -> Int {
        collectionCellControllers.count
    }
    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        collectionCellControllers[section].count
    }
    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        collectionCellControllers[indexPath.section][indexPath.row].cellFromCollectionView(collectionView, forIndexPath: indexPath)
    }
}

extension UserCollectionViewController: UICollectionViewDelegateFlowLayout {
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        collectionCellControllers[indexPath.section][indexPath.row].sizeForItem(collectionView: collectionView)
    }
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
        collectionCellControllers[section][0].insetForSection(collectionView: collectionView)
    }
}

Hope this helps you in your current and coming projects. Thanks for reading.

You can check full demo from here: