How to create a static UICollectionView

UICollectionView doesn’t support static content layouts as its sibling UITableView. There is a way to simulate it though, and this article will walk through how to do just that.

Tutorial objectives

When using a UITableView, we have a choice of either creating a table with static cells or to create cell templates that are used to create cells dynamically. The former approach can be used to more quickly implement simple use cases like a screen to allow users to change settings. The latter dynamic approach is used when the number of cells are not known until runtime–such as a list of products in a product catalog stored on a web service.

In this tutorial, we’ll create a UICollectionView that will serve as a “main menu” for an application. We’ll provide the following functionality to the application:

  1. The collection view will display a fixed number of cells designed within an Xcode storyboard.
  2. The cells will adapt to the size of the display — for example using a two-column layout on an iPhone X in portrait mode, and a single column on an iPhone SE in portrait mode. In landscape mode, the click-able cells will fill the horizontal space before starting a new row.
  3. When the user taps on a cell, the application will intercept that event and respond appropriately (in a production app, this may be to fire the correct segue associated with each cell.

To keep the tutorial simple, this application will just display four cells of identical size and layout. But a real application could be much more sophisticated. Each cell could be a different Collection View Cell design, and present entirely different content from the others. But the basic architecture for this approach would be the same.

Step 1: Creating a static layout in Interface Builder

As with creating static UITableView layouts, the first step is to create a layout in Interface Builder.

Complete the following steps first:

  1. Open Xcode and create a new single view application
  2. Add a UICollectionView to the View Controller scene in Main.storyboard
  3. Set ViewController as the UICollectionView delegate and datasource
  4. Using the size inspector, customize the UICollectionView cell size to w=170, h=80

Customize the default UICollectionViewCell with the following changes:

  1. Add a UIView, and use constraints to pin it 4 points from the top, bottom, leading and trailing edges (clear the constrain to margins checkbox).
  2. Add a UILabel to the UIView in step 1, and center it vertically & horizontally in the UIView container.
  3. Change the background color of the UIView to Purple, and the UILabel text to “Purple Cell”.
  4. Using the Identity Inspector, change the UICollectionViewCell Collection Reusable View Identifier to “Purple Cell”.

Now copy & paste the Purple cell three times. Change the UIView color, the UILabel text and the UICollectionViewCell Reuse Identity to differentiate each of the four cells from each other. When finished your storyboard design should look something like this:

Step 2: Implement the view controller delegates

If you ran the application now, you’d see a screen with an empty UICollectionView. Why is that? It’s because all we really did was to design some templates of what a set of dynamic cells can look like. Even though the layout looks similar to what can be done using a static UITableView (except for multiple columns), it’s not really a static design. But by adding two data source methods, you can provide the missing information to create the layout design using Interface Builder at runtime.

Change the UIViewController class definition as follows:

class ViewController: UIViewController { let cellIds = ["Purple Cell","Green Cell","Blue Cell","Red Cell"] let cellSizes = Array( repeatElement(CGSize(width:170, height:80), count: 4)) override func viewDidLoad() { super.viewDidLoad() } }

The cellIds property contains a list of the Identity properties we assigned to each UICollectionViewCell designed in Interface builder. These Ids must exactly match the values we assigned to each cell in Interface Builder.

The cellSizes property stores the size of each cell. In this simple tutorial, all cells will be the same size–but they don’t have to be; each cell could have different content and a different size. By defining the sizes here, we’re giving ourselves the ability to control cell sizes at runtime via the UICollectionViewDelegateFlowLayout (we’ll do this in a moment).

Add data source delegate methods

Now add the data source methods to the end of the ViewController.swift file via a swift extension.

extension ViewController: UICollectionViewDataSource { func collectionView( _ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return cellIds.count } func collectionView( _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { return collectionView.dequeueReusableCell( withReuseIdentifier: cellIds[indexPath.item], for: indexPath) } }

Each of these delegate methods has only one line of code, and accomplish the following:

  1. numberOfItemsInSection returns the number of cells in the UICollectionView, which is inferred by the number of cell Ids we added to the cellIds property in the last step.
  2. For each of the cells, we create a cell with the corresponding Id. This is the workaround that allows us to make a dynamic UICollectionView behave like a static UITableView.

Add the layout delegate method

To give us control over the size of each cell at runtime, we can adopt the UICollectionViewDelegateFlowLayout protocol, and provide an implementation for sizeForItemAt method. The implementation will simply return the corresponding cellSizes element corresponding to the indexPath being laid out.

extension ViewController: UICollectionViewDelegateFlowLayout { func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { return cellSizes[indexPath.item] } }

Add the didSelectItemAt delegate method

Since the objective of this tutorial was to create a type of menu, we need to intercept when users tap on items in the menu. To do this, implement a single UICollectionViewDelegate method. Add the following extension to the bottom of ViewController.swift.

extension ViewController: UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { print("User tapped on (cellIds[indexPath.row])") } }

This delegate method is called when the user taps on any of the (four) cells in the UICollectionView. In response, the sample code just prints the Cell Id of the cell the user tapped on. In a production application, we might instead cast the UITableViewCell to a custom class, and read metadata from it to decide how to branch the application flow.

Now if you run the application, and press on each button, you should see the following output in the Xcode debug console.

User tapped on Purple Cell User tapped on Green Cell User tapped on Red Cell User tapped on Blue Cell

The completed, flexible layout

If you now run the application on different devices, you can see that we’ve created an almost static UICollectionView that represents a menu. The advantage of this approach over using a UITableView is that we have a much more flexible layout that can be presented on different devices.

iPhone X & iPhone SE — Portrait

The Width of the iPhone X and iPhone SE are quite different, so our layout automatically adapts between two columns (X) and 1 column (SE).

iPhone X — Landscape

The iPhone X in landscape has plenty of horizontal space, so all four of the menu items fit on one row.

iPhone SE — Landscape

The iPhone SE in landscape is more constrained horizontally, so the layout automatically flows onto two rows:

Changing up the cell sizes

One of the advantages to UICollectionView is how flexible it is when cell sizes are different. By changing the array of cell sizes at the top of UIViewController.swift to the following, we can observe this flexibility in action.

Change the cellSizes property in ViewController.swift to the following:

let cellSizes = [ CGSize(width:210, height:60), CGSize(width:180, height:100), CGSize(width:170, height:80), CGSize(width:150, height:150) ]

Full source code

I hope this tutorial was helpful to you and gave you some ideas for your own applications. You can download the full source code for this tutorial here on my github account. Feel free to contact me on twitter via @rekerrsive.

Leave a Reply

Your email address will not be published. Required fields are marked *