Published on

Integrating UICollectionView with UITableView Programmatically

Most of the tutorials and guidelines focus on using a storyboard or an .xib file for each view controller or UIView object and that includes interacting with UITableView and UICollectionView. In this walkthrough, I decided not to use a storyboard to implemented a horizontally scrollable collection of cells inside a UITableView but instead define the required variables and delegates and set the constraints programmatically.

Requirements

  • iOS 13
  • Swift 5
  • Xcode 13.1

Final Result

Final Result


Housekeeping

To begin with this demo, create a fresh iOS project and set the minimum deployment target as iOS 13 in both the project (Info tab) and the target (General tab).

  • Project Info tab

Project Info Tab

  • Target general settings

Target General Settings

Also, we need — which is optional — two sets of swift file that store the data for the table cells and constants for the height and width of the table cells. For that to happen, create two swift files and name them Content and Configuration

  • For Content.swift
// Content.swift

struct Content {
    
    let id: UUID
    let title: String
    let date: Date
    let imageTitle: String
    
    init(title: String, date: Date = Date(), imageTitle: String) {
        self.id = UUID()
        self.title = title
        self.date = date
        self.imageTitle = imageTitle
    }
    
}
  • For Configuration.swift
//  Configuration.swift

import UIKit

enum TableLayoutConstant {
    static let spacing: CGFloat = 16.0
    static let itemHeight: CGFloat = 250
    static let itemWidth: CGFloat = 200.0
}

enum LayoutConstants {
    
		//MARK: Collection View Cell
    static let viewCornerRadius: CGFloat = 4.0

    static let imageHeight: CGFloat = 180.0

    static let topPadding: CGFloat = 10.0
    static let verticalSpacing: CGFloat = 8.0
    static let horizontalPadding: CGFloat = 10.0
    static let profileDescriptionVerticalPadding: CGFloat = 8.0
    
}

Replacing Storyboard

Since we are not going to implement storyboard or any .xib in this demo, we need to remove the main.storyboard and the default ViewController.swift that comes with a fresh iOS project. When these two files are removed, we will replace them with a new class that has a subclass of UITableViewController

To remove main.storyboard :

  • Go to the project targets and remove Main from Main Interface dropdown menu

Remove Main Interface

  • Type main in the search box and from the result check the info.plist

Search Box Main

  • Remove Storyboard Name entry from info.plist

Remove Info Entry

It’s time now to add a new entry point for our demo. To do so, we are going to create a new Cocoa Touch class that will be named HomeTableViewController with a subclass of UITableViewController

  • Choosing Cocoa Touch Class

Cocoa Touch Class

  • Naming the file and adding a subclass.

File Name Subclass

When the new file is created, head over to SceneDelegate to adjust and include our new HomeTableViewController.swift as the main entry point with UINavigationController.

//  SceneDelegate.swift

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let windowScene = (scene as? UIWindowScene) else { return }
        
        window = UIWindow(windowScene: windowScene)
        window?.rootViewController = UINavigationController(rootViewController: HomeTableViewController())
        window?.makeKeyAndVisible()
        
    }
}

Main Entry Point (HomeTableViewController) Setup

Head over to HomeTableViewController.swift to add a variable as an array to hold our data and run the app to test if the new entry point is working as expected.

  • Add a new variable contents
  • Set the view background color
  • Set a navigation title for the view controller
//  HomeTableViewController.swift

import UIKit

class HomeTableViewController: UITableViewController {

    private lazy var contents = [Content]()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
		// Set background color for the main view
        view.backgroundColor = .systemBackground
        
        setupNavigation()
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
    }
    
    private func setupNavigation() {
        
		// Set title for the navigation bar
        title = "Home Title"
        
		// Set navigation bar title as a large title
        navigationItem.largeTitleDisplayMode = .automatic
        navigationController?.navigationBar.prefersLargeTitles = true
    
		// Include a button in the right corner of the navigation bar
        navigationItem.rightBarButtonItem = UIBarButtonItem(
          image: UIImage(systemName: "plus.circle"),
          style: .done,
          target: self,
          action: #selector(didTapNewItemButton)
        )
        navigationItem.rightBarButtonItem?.accessibilityLabel = "New Home Item"
        
    }
    
    @objc func didTapNewItemButton() {
        
    }
    
}

When you run the app, the output has to include a large title and a plus button at the right hand corner of the navigation bar (Testing against iPhone 12).

Init Output

Application Flow

We are going to have three different swift files to produce the result below.

Application Flow

Here is a breakdown for all the three swift files:

  • HomeTableViewController (the entry point) which will define all necessary delegates for UITableView and register a table view cell.
  • HomeTableViewCell is where the collection view will be defined and all necessary delegates for UICollectionView will be created. The collection view in this file will register a collection view cell.
  • HomeCollectionViewCell will contain the layout for an individual card (see below) and how the data will be represented.

Card Layout

HomeTableViewController - Entry Point Setup

HomeTableViewController is the entry point of the app which will be used to setup all the required delegates for the UITableViewController and register a table view cell. Since the class is a subclass of UITableViewController , we do not need to inherit UITableViewDelegate and UITableViewDataSource.

Using extension to organize the code base, we are going to include all the required functions for the UITableViewDelegate and UITableViewDataSource

// HomeTableViewController.swift

//MARK: - UITable Delegates
extension HomeTableViewController {

	// How many rows that table will have
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 1
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        return UITableViewCell()
    }
	
	// The height of each row  
    override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 300
    }

}

Few points to go though:

  • The number of rows will be fixed to one as we are going to use the first row to display a collection view.
  • In cellForRowAt function, initially we are going to return a UITableViewCell until we create a new subclass that inherits it. The function will be updated accordingly.

HomeTableViewCell - Cells for the Table View

We need to create a new class that inherits UITableViewCell and will be used to replace the values in override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath)

To create the file, create a Cocoa Touch class with a subclass of UITableViewCell

Cocoa Touch Class View Cell

In this new file, we will create the following variables:

  • a cell identifier to be used later on HomeTableViewController
  • a private lazy collection view variable that will be used to register UICollectionViewCell
  • an items variable that will be holding the data that will populate the collectionview
  • an init function
//  HomeTableViewCell.swift
class HomeTableViewCell: UITableViewCell {
    
    // cell identifier
    static let reuseIdentifier = String(describing: HomeTableViewCell.self)
		
    // Collection View
    private lazy var collectionView: UICollectionView = {
        
        let flowLayout = UICollectionViewFlowLayout()
        flowLayout.scrollDirection = .horizontal // Sets the scrolling direction for the collection view as horizontal
        
        let collection = UICollectionView(frame: .zero, collectionViewLayout: flowLayout)
        collection.translatesAutoresizingMaskIntoConstraints = false
        collection.isScrollEnabled = true
        collection.backgroundColor = .systemBackground

        return collection
        
    }()

    var items = [Content]()

    // Initialization for the class
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

Next, we need to include UICollectionView delegates as extensions

  • UICollectionViewDataSource
    • initially, cellForItemAt will return UICollectionViewCell() until the class that inherits from UICollectionViewCell is available.
    • numberOfItemsInSection will have the count of items that will be displayed in each collection view cell.
//MARK: - UICollectionViewDataSource
extension HomeTableViewCell: UICollectionViewDataSource {

    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        // process the selected item
    }
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
		// will be used to return how many items are in the collection cell
        return items.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        return UICollectionViewCell()
    }

}
  • UICollectionViewDelegateFlowLayout
//MARK: - UICollectionViewDelegateFlowLayout
extension HomeTableViewCell: UICollectionViewDelegateFlowLayout {
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        return CGSize(width: TableLayoutConstant.itemWidth, height: TableLayoutConstant.itemHeight)
    }
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
        return UIEdgeInsets(
            top: TableLayoutConstant.spacing,
            left: TableLayoutConstant.spacing,
            bottom: TableLayoutConstant.spacing,
            right: TableLayoutConstant.spacing
        )
    }
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
        return TableLayoutConstant.spacing
    }
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
        return TableLayoutConstant.spacing
    }
    
}

Now we have a collection view variable that needs to be added as a subview to the main UIView of the table cell. In order to do that, we need to add an initialization function the HomeTableViewCell

//  HomeTableViewCell.swift

class HomeTableViewCell: UITableViewCell {

	...

	override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {

        super.init(style: style, reuseIdentifier: reuseIdentifier)

        setupViews()
        setupTableCellLayout()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

	private func setupViews() {
        selectionStyle = .none

		// Add collectionView as subview to the contentView (Main UIView)
        contentView.addSubview(collectionView)
        
        collectionView.register(
            HomeCollectionCellView.self,
            forCellWithReuseIdentifier: HomeCollectionCellView.reuseIdentifier
        )
      
        // Assign delegates and dataSource - which has been defined in the previous step
        collectionView.delegate = self
        collectionView.dataSource = self
    }
  
    private func setupTableCellLayout() {

        // Add the required constraints to the collectionView
        NSLayoutConstraint.activate([
            collectionView.topAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.topAnchor),
            collectionView.bottomAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.bottomAnchor),
            collectionView.leadingAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.leadingAnchor),
            collectionView.trailingAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.trailingAnchor),
        ])
    }

}

HomeCollectionViewCell - Cells for the Collection View

Let’s create a new file called HomeCollectionViewCell that is a subclass of UICollectionViewCell.

Cocoa Touch Class Collection Cell

In the new class, we will define how the data will be represented. The data consists of a title, a subtitle (a date) and an image. To define these properties, the following lazy variables will be created:

  • titleLabel
  • subtitleLabel
  • imageView
  • dateFormatter
  • a cell identifier which will be used to register this class as a cell for the collection view.
class HomeCollectionViewCell: UICollectionViewCell {
    
    
    private lazy var titleLabel: UILabel = {
        
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        label.textAlignment = .left
        label.numberOfLines = 0
        label.font = UIFont.systemFont(ofSize: 18, weight: .medium)
        label.textColor = .black
        label.layer.borderWidth = 2
        label.layer.borderColor = CGColor(red: 255.0, green: 240.0, blue: 123.0, alpha: 1)
        return label
        
    }()
    
    private lazy var subtitleLabel: UILabel = {
        
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        label.font = UIFont.preferredFont(forTextStyle: .subheadline)
        label.textColor = .secondaryLabel
        label.numberOfLines = 1
        label.adjustsFontForContentSizeCategory = true
        label.isAccessibilityElement = false
        return label
        
    }()
    
    private lazy var imageView: UIImageView = {
        
        let imageView = UIImageView(frame: .zero)
        imageView.contentMode = .scaleAspectFit
        imageView.translatesAutoresizingMaskIntoConstraints = false
        return imageView
        
    }()
    
    private lazy var dateFormatter: DateFormatter = {
        
        let dateFormatter = DateFormatter()
        dateFormatter.dateStyle = .long
        return dateFormatter
        
    }()

    
    static let reuseIdentifier = String(describing: HomeCollectionViewCell.self)
    
}

Next step, we need to include an initialiaztion function that will setup the views and constraints for this collection cell

class HomeCollectionViewCell: UICollectionViewCell {
    
    ...

    override init(frame: CGRect) {
        super.init(frame: .zero)
        
        setupViews()
        setupLayoutConstraint()
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private func setupViews() {
        contentView.clipsToBounds = true
        contentView.layer.cornerRadius = LayoutConstants.contentViewCornerRadius
        contentView.backgroundColor = .white
        
        contentView.layer.borderWidth = 2
        contentView.layer.borderColor = CGColor(red: 111/255, green: 138/255, blue: 183/255, alpha: 1)
                
        contentView.addSubview(imageView)
        contentView.addSubview(titleLabel)
        contentView.addSubview(subtitleLabel)
    }
    
    private func setupLayoutConstraint() {
        
        // Constraints for Image View
        NSLayoutConstraint.activate([
            imageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
            imageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
            imageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: LayoutConstants.topPadding),
            imageView.heightAnchor.constraint(equalToConstant: LayoutConstants.imageHeight)
        ])
        
        // Constraints for Title View
        NSLayoutConstraint.activate([
            titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
            titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: LayoutConstants.horizontalPadding),
            titleLabel.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: LayoutConstants.profileDescriptionVerticalPadding)
        ])
        
        // Constraints for Subtitle View
        NSLayoutConstraint.activate([
            subtitleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
            subtitleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: LayoutConstants.horizontalPadding),
            subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 4.0)
        ])
    }
}

Register the Cells (Table Cell and Collection Cell)

With all the required configurations out of the way (except for one), we can register the cells to their respective views i.e. Table Cell for Table View and Collection Cell for Collection View.

Let’s start with HomeTableViewController in viewDidLoad() function:

//  HomeTableViewController.swift

class HomeTableViewController: UITableViewController {
		
    ...

    override func viewDidLoad() {
        super.viewDidLoad()
    
        ...

        // Register the table cell
        tableView.register(HomeTableViewCell.self, forCellReuseIdentifier: HomeTableViewCell.reuseIdentifier)
    }

    ...

}

Next, it’s the time for HomeTableViewCell to register the collection cell under setupView() function.

//  HomeTableViewCell.swift

class HomeTableViewCell: UITableViewCell {
		
    ...

    private func setupView(){
    
        ...

        // Register the collection cell
        collectionView.register(HomeCollectionViewCell.self, forCellWithReuseIdentifier: HomeCollectionViewCell.reuseIdentifier)
    
        ...        
    }

    ...

}

Before we pass the data to the table and collection views, we need to tell each cell (Table Cell and Collection Cell) which data to use. In order to do so:

  • head to HomeCollectionViewCell and add a configure function.
//  HomeCollectionViewCell.swift

class HomeCollectionViewCell: UICollectionViewCell {

    ... 

    public func configure(with item: Content) {
        titleLabel.text = item.title
        
        let subtitleText = dateFormatter.string(from: item.date)
        subtitleLabel.text = subtitleText
        
        let desiredImage = UIImage(named: item.imageTitle)
        imageView.image = desiredImage
    
    }

}
  • from there, go to HomeTableViewCell and add another configure function.
//  HomeTableViewCell.swift

class HomeTableViewCell: UITableViewCell {
		
    ...

    public func configure(with items: [Content]) {
        self.items = items
                // Reload the collection view
        collectionView.reloadData()
    }

}

The last step is to pass the data from table view to table cell and from collection view to collection cell. Head to:

  • the UITableDelegates extension under HomeTableViewController and change the return type for cellForRowAt function.
//  HomeTableViewController.swift

//MARK: - UITable Delegates
extension HomeTableViewController {
			
    ...

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
        let cell = tableView.dequeueReusableCell(withIdentifier: HomeTableViewCell.reuseIdentifier, for: indexPath) as! HomeTableViewCell
        
        cell.configure(with: contents)
        
        return cell
    }

    ...

}
  • the UICollectionViewDataSource extension under HomeTableViewCell and change the return type for cellForRowAt function.
//  HomeTableViewCell.swift

//MARK: - UICollectionViewDataSource
extension HomeTableViewCell: UICollectionViewDataSource {

    ...

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: HomeCollectionViewCell.reuseIdentifier, for: indexPath) as! HomeCollectionViewCell
        
        cell.configure(with: items[indexPath.row])
        
        return cell
        
    }

}

Random Data

With the completion of all the required configurations, it’s time to populate the table with some random data. Head to viewWillappear() function under the main class HomeTableViewController and append random data to contents array.

//  HomeTableViewController.swift

class HomeTableViewController: UITableViewController {

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        // Populate the contents array
        contents.append(Content(title: "New Title", imageTitle: "image02.jpeg"))
        contents.append(Content(title: "New Title", imageTitle: "image03.jpeg"))
    }

}

Build and run (⌘ + R) the app to see the final result.