Swift 5: UICollectionView with multiple cell types

Anastasiia
5 min readAug 2, 2022

--

Preface

I’m not a brilliant designer. My task is to show how to use different cells on the same CollectionView. I have learned this way through my work. This is my first article, so please, if you have any comments, contact me. Thank you and have a nice reading ❤️

Set up project

The application consists of a single UICollectionViewController. The first cell is just a picture elongated in height, the second is a label, the third is a collection with pictures, the fourth (footer) is a label and two buttons.

iPod touch 7th generation

Before we start:

  1. The app will be created programmatically. Remove storyboard (this article will help you)
  2. To work with constraints, I will use SnapKit
  3. To hide navigation bar in viewWillAppear write
navigationController?.setNavigationBarHidden(true, animated: animated)

4. The images are saved in assets. For convenience, I will get access to the pictures through the extension UIImage (similarly for color):

extension UIImage { enum Photos {  static let sea = UIImage(named: "sea")  static let bridge = UIImage(named: "bridge")  static let dandelion = UIImage(named: "dandelion")  static let mountain = UIImage(named: "mountain")  static let killer_whale = UIImage(named: "killer_whale")  static let polar = UIImage(named: "polar")  static let river = UIImage(named: "river")  static let winter = UIImage(named: "winter")  static let forest_river = UIImage(named: "forest_river") } enum Icons {  static let tg_icon = UIImage(named: "tg")  static let in_icon = UIImage(named: "in") }}

5. Some constants are stored in enum Constants:

enum Constants { static let height = UIScreen.main.bounds.height static let width = UIScreen.main.bounds.width static let photosCellWidth = width static let photosCellHeight = photosCellWidth / 1.5 static let iconSize = width / 6}

Write code

First view and CollectionViewController

let’s create a view for the first collection cell. In this view , we will have only one UIImageView. During initialization, we will add imageView to the screen and set up constraints. It is important for the view to set the width equal to the width of the screen. ImageView is made according to the size of the entire view and set the height. We will also need the ID to register the future cell.View for the first cell will look like:

final class FirstView: UIView {
static let id = "FirstView"
private lazy var imageView: UIImageView = { let imageView = UIImageView() imageView.image = .Photos.mountain imageView.contentMode = .scaleToFill return imageView }() override init(frame: CGRect) { super.init(frame: frame) setupView() } public required init?(coder: NSCoder) { super.init(coder: coder) setupView() } private func setupView() { addSubview(imageView) setupConstraints() } private func setupConstraints() { snp.makeConstraints { make in make.width.equalTo(Constants.width) } imageView.snp.makeConstraints { make in make.edges.equalToSuperview() make.height.equalTo(Constants.height * 0.7) } }}

Before adding the view as a cell to the CollectionViewController, let’s configure the CollectionViewController. In init create layout and pass it in super.init:

init() { let layout = UICollectionViewFlowLayout() layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize super.init(collectionViewLayout: layout)}

UICollectionViewFlowLayout.automaticSize will automatically calculate the cell size depending on the content.

Now we need to create a cell that will contain FirstView (and all next views).

class CollectionViewCell: UICollectionViewCell { var view = UIView() func setupCell(view: UIView) {  self.view = view  contentView.addSubview(view)  view.snp.makeConstraints { make in  make.edges.equalToSuperview()  } }}

Now we have a cell that will adjust to the size of the content.

We do the registration in this way:

collectionView.register(CollectionViewCell.self, forCellWithReuseIdentifier: FirstView.id)

In numberOfItemsInSection return count of views (now it is 1) and in cellForItemAt put in cell the view:

public override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { guard let cell =     collectionView.dequeueReusableCell(withReuseIdentifier:  FirstView.id, for: indexPath) as? CollectionViewCell else { return UICollectionViewCell() } cell.setupCell(view: FirstView()) return cell}

We get (I have added a black background to the collectionView so that you can see the borders of the screen):

Second view

In the same way, create a second view with a label. Constraints in the view look like this:

snp.makeConstraints { make in make.width.equalTo(Constants.width)}label.snp.makeConstraints { make in make.centerY.top.bottom.equalToSuperview() make.leading.trailing.equalToSuperview().inset(15)}

For convenience in CollectionViewController add:

private lazy var firstView = FirstView()private lazy var secondView = SecondView()private lazy var cells: [(view: UIView, id: String)] = [                         (firstView, FirstView.id),                         (secondView, SecondView.id)]

And numberOfItemsInSection and cellForItemAt:

 public override func collectionView(_ collectionView:  UICollectionView, numberOfItemsInSection section: Int) -> Int { cells.count}public override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { guard let cell =  collectionView.dequeueReusableCell(withReuseIdentifier:  cells[indexPath.row].id, for: indexPath) as? CollectionViewCell else  { return UICollectionViewCell() } cell.setupCell(view: cells[indexPath.row].view) return cell}

And we get:

Third view

The third view will consist of a collection view. Set scrollDirection to horizontal, add minimumLineSpacing equal 3. We get:

final class ThirdView: UICollectionView { static let id = "ThirdCell" private let photos: [UIImage?] = [.Photos.sea,.Photos.bridge,.Photos.dandelion,.Photos.killer_whale,.Photos.polar,.Photos.winter,.Photos.river,.Photos.forest_river] init() { let layout = UICollectionViewFlowLayout() layout.scrollDirection = .horizontal layout.minimumLineSpacing = 3 super.init(frame: .zero, collectionViewLayout: layout) setupView()}required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented")}private func setupView() { showsHorizontalScrollIndicator = false register(PhotosCell.self, forCellWithReuseIdentifier: PhotosCell.id) dataSource = self delegate = self }}
extension
ThirdView: UICollectionViewDataSource {
public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { photos.count} public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: PhotosCell.id, for: indexPath) as? PhotosCell else { return UICollectionViewCell() } cell.addImage(image: photos[indexPath.row]) return cell }}extension ThirdView: UICollectionViewDelegateFlowLayout { func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { CGSize(width: Constants.photosCellWidth, height: Constants.photosCellHeight) }}

And PhotosCell:

final class PhotosCell: UICollectionViewCell { static let id = "PhotosCell" private lazy var imageView: UIImageView = {  let imageView = UIImageView()  imageView.contentMode = .scaleToFill  return imageView}() public override init(frame: CGRect) {  super.init(frame: frame)  setupView()} required init?(coder: NSCoder) {  fatalError("init(coder:) has not been implemented")} func addImage(image: UIImage?) {  guard let image = image else { return }  imageView.image = image} private func setupView() {  addSubview(imageView)  setupConstraints()} private func setupConstraints() {  imageView.snp.makeConstraints { make in   make.edges.equalToSuperview()  } }}

Result:

Footer

And finally: add a footer.

In the usual way, create a view, inherit from UICollectionReusableView and add it to the collectview as follows:

collectionView.register(FooterCell.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: FooterCell.id)

Set view for footer and set footer size:

extension CollectionViewController: UICollectionViewDelegateFlowLayout { override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {  switch kind {   case UICollectionView.elementKindSectionFooter:    let footerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: FooterCell.id, for: indexPath)    return footerView   default:    return UICollectionReusableView()   } } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize {  CGSize(width: UIScreen.main.bounds.width, height: 120) }}

Return the background color to normal.

That is it!

I hope this article helped you. You can find the complete project here. Thanks for reading.

--

--