You are currently viewing Designing Custom UICollectionViewListCell in Interface Builder

Designing Custom UICollectionViewListCell in Interface Builder

Since the publishing of my article “UICollectionView List with Custom Cell and Custom Configuration“, a lot of my readers have been asking me whether it is possible to design the custom UICollectionViewListCell in the interface builder.

Well, the answer is definitely yes!

Since the UICollectionViewListCell‘s content view is just a UIView subclass that conforms to the UIContentView protocol, theoretically as long as we are able to link up the content view with the interface builder then everything should work accordingly.

After spending some time experimenting in Xcode, I have successfully created a UICollectionViewListCell content view using interface builder. On top of that, I also discovered that you can even manipulate the cell height by adjusting the auto layout constraints within the cell’s content view.

How can this be done? Let’s find out.


The Sample App

For simplicity’s sake, our sample app will show a simple custom cell that contains only 1 label. Here’s the screenshot of the sample app:

Designing Custom UICollectionViewListCell in Interface Builder
The sample app we going to build

Note that I also added a red border to the label so that it is easier for us to observe its height later in this article.

With all that being said, let’s begin.

Note:

I won’t get into too much detail on the concept behind content view, content configuration, and custom cell. If you are not familiar with that, I highly recommend you to go through my previous article “UICollectionView List with Custom Cell and Custom Configuration” before proceeding.


Implementing Custom Content View and Content Configuration

First thing first, let’s implement the custom content view and content configuration. Let’s name both of them SFSymbolNameContentView and SFSymbolNameContentConfiguration.

Here’s how we will implement the SFSymbolNameContentView class:

class SFSymbolNameContentView: UIView, UIContentView {
    
    // 1
    // IBOutlet to connect to interface builder
    @IBOutlet var containerView: UIView!
    @IBOutlet var nameLabel: UILabel!
    
    private var currentConfiguration: SFSymbolNameContentConfiguration!
    var configuration: UIContentConfiguration {
        get {
            currentConfiguration
        }
        set {
            // Make sure the given configuration is of type SFSymbolNameContentConfiguration
            guard let newConfiguration = newValue as? SFSymbolNameContentConfiguration else {
                return
            }
            
            // Set name to label
            apply(configuration: newConfiguration)
        }
    }
    
    init(configuration: SFSymbolNameContentConfiguration) {
        super.init(frame: .zero)
        
        // 2
        // Load SFSymbolNameContentView.xib
        loadNib()
        
        // Set name to label
        apply(configuration: configuration)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
}

private extension SFSymbolNameContentView {
    
    private func loadNib() {
        
        // 3
        // Load SFSymbolNameContentView.xib by making self as owner of SFSymbolNameContentView.xib
        Bundle.main.loadNibNamed("\(SFSymbolNameContentView.self)", owner: self, options: nil)
        
        // 4
        // Add SFSymbolNameContentView as subview and make it cover the entire content view
        addSubview(containerView)
        containerView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            containerView.topAnchor.constraint(equalTo: self.topAnchor, constant: 0.0),
            containerView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 0.0),
            containerView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: 0.0),
            containerView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: 0.0),
        ])
        
        // 5
        // Add border & rounded corner to name label
        nameLabel.layer.borderWidth = 1.5
        nameLabel.layer.borderColor = UIColor.systemPink.cgColor
        nameLabel.layer.cornerRadius = 5.0
    }
    
    private func apply(configuration: SFSymbolNameContentConfiguration) {
    
        // Only apply configuration if new configuration and current configuration are not the same
        guard currentConfiguration != configuration else {
            return
        }
        
        // Replace current configuration with new configuration
        currentConfiguration = configuration
        
        // Set name to label
        nameLabel.text = configuration.name
    }
}

From the above code, there are several important parts that we must take note:

  1. Define IBOutlet for the label and container view. Note that the container view will be our interface builder canvas, we will be working on that in just a moment.
  2. Call the loadNib() function during initialization to configure SFSymbolNameContentView accordingly.
  3. Within the loadNib() function, load a nib file named SFSymbolNameContentView.xib and make self as the owner of the nib file.
  4. Make the container view take up the entire content view using auto layout constraints.
  5. Add a red color border and rounded corners to the label.

Next up, we will work on the implementation of SFSymbolNameContentConfiguration class. The implementation is pretty straightforward, we just need to make sure the makeContentView()is returning an instance of SFSymbolNameContentView.

struct SFSymbolNameContentConfiguration: UIContentConfiguration, Hashable {

    var name: String?
    
    func makeContentView() -> UIView & UIContentView {
        // Initialize an instance of SFSymbolNameContentView
        return SFSymbolNameContentView(configuration: self)
    }
    
    func updated(for state: UIConfigurationState) -> Self {
        return self
    }
}

Creating Custom UIView in Interface Builder

With both content view and content configuration classes in place, let’s switch our focus to the interface builder. Here’s what we are going to do:

  1. Create a new user interface view.
  2. Set the file’s owner’s class.
  3. Resize the custom view.
  4. Add a label and configure auto layout.
  5. Connecting all IBOutlets.

1. Create a New User Interface View

In Xcode, press ⌘N (cmd + N) to add a new file to your project. In the add new file dialog, select “View” under the user interface section, click “Next” and name the nib file SFSymbolNameContentView.

Add new nib file in Xcode
Add new nib file in Xcode

2. Set the File’s Owner’s Class

Next, we must let the interface builder know that the nib file we just created is owned by the SFSymbolNameContentView class.

Inside the interface builder left panel, click on “File’s Owner” and then within the attribute inspector, set the file’s owner’s class to SFSymbolNameContentView.

Setting file's owner in Xcode interface builder
Setting file’s owner

3. Resize the Custom View

By default, the interface builder will create a custom view following the iPhone form factor. However, this is not what we want, we need a view much smaller than that.

Go ahead and open the view’s attribute inspector and change the size simulated metrics to “freeform“.

Setting view's size simulated metrics size to freeform in Xcode
Setting view’s size simulated metrics size to freeform

After that, open the size inspector and set the view height to 150pt.

Setting view's height in Xcode
Setting view’s height to 150pt

4. Add a Label and Configure Auto Layout

Now drag a UILabel into the view and set its top, bottom, leading, and trailing auto layout constraint according to the image below.

Setting top, bottom, leading and, trailing constrains in Xcode
Setting label’s top, bottom, leading and, trailing constrains
Auto layout constraints in Xcode
The label’s auto layout constraints

This will make the UILabel take up the entire view.

5. Connecting All IBOutlets

Lastly, connect the containerView and nameLabel IBOutlet to the interface builder as shown in the image below:

Connecting IBOutlets in Xcode interface builder
Connecting the IBOutlets

With that, we have successfully connected the SFSymbolNameContentView class to the interface builder. Let’s go ahead and run your sample app to see everything in action.

But wait! The cell height is different from what we expected! 😨

Unexpected cell height in UICollectionView list
The unexpected cell height

In the next section, let’s find out what causes this problem and how we can fix it.


Dealing with Cell Height

The reason behind the unexpected cell height is because UICollectionViewListCell is self-sizing by default.

Since we do not specify the height constraint of the nameLabel and we only configure the nameLabel to take up the entire containerView, the collection view will automatically resize the cells based on the nameLabel‘s content.

The solution to fix the problem is pretty straightforward. We just need to add a height constraint to the nameLabel so that the auto layout engine will take into account the nameLabel‘s height when calculating the cell’s height.

Adding height constraint to label
Adding height constraint to nameLabel

If you run your sample app now, you should see that the cell has been resized correctly. Unfortunately, by adding a height constraint to the nameLabel has caused unsatisfiable constraints within the cell.

(
    "<NSAutoresizingMaskLayoutConstraint:0x6000013f44b0 h=--& v=--& Swift_Senpai_UICollectionView_List.SFSymbolNameContentView:0x7ff888d29b60.height == 44   (active)>",
    "<NSLayoutConstraint:0x600001388a00 UILabel:0x7ff88b020250.height == 126   (active)>",
    "<NSLayoutConstraint:0x600001388c30 UILayoutGuide:0x6000009bd0a0'UIViewSafeAreaLayoutGuide'.bottom == UILabel:0x7ff88b020250.bottom + 12   (active)>",
    "<NSLayoutConstraint:0x600001388c80 V:|-(12)-[UILabel:0x7ff88b020250]   (active, names: '|':UIView:0x7ff88b0200e0 )>",
    "<NSLayoutConstraint:0x600001388d20 V:|-(0)-[UIView:0x7ff88b0200e0]   (active, names: '|':Swift_Senpai_UICollectionView_List.SFSymbolNameContentView:0x7ff888d29b60 )>",
    "<NSLayoutConstraint:0x600001388e10 UIView:0x7ff88b0200e0.bottom == Swift_Senpai_UICollectionView_List.SFSymbolNameContentView:0x7ff888d29b60.bottom   (active)>",
    "<NSLayoutConstraint:0x600001388b90 'UIViewSafeAreaLayoutGuide-bottom' V:[UILayoutGuide:0x6000009bd0a0'UIViewSafeAreaLayoutGuide']-(0)-|   (active, names: '|':UIView:0x7ff88b0200e0 )>"
)

Will attempt to recover by breaking constraint 
<NSLayoutConstraint:0x600001388a00 UILabel:0x7ff88b020250.height == 126   (active)>

Based on the above Xcode console output, it seems like the nameLabel‘s height constraint is conflicting with the content view’s height constraint.

Unsatisfiable constraints error log in Xcode console
Unsatisfiable constraints error log in Xcode console

“But we never set any height constraint on the content view!” you might say.

I am not 100% sure on why there is a height constraint being set on the content view, I suspect UIKit is giving the content view a default 44pt height constraints before calculating its actual height.

Since we set our nameLabel to have 126pt height, but the content view can only have 44pt height, it is not possible to simultaneously satisfy both of these constraints, thus causing the unsatisfiable constraints issue.

To resolve this issue, we can reduce the nameLabel‘s height constraint priority to 999 so that the auto layout engine can temporarily ignore this constraint.

Setting constraint priority in Xcode interface builder
Setting nameLabel height constraint priority to 999

This article discusses in great details the solution we use to resolve the unsatisfiable constraints issue. Feel free to check it out if you want to know more.

With that, we have successfully designed a custom UICollectionViewListCell using an interface builder. 🥳

You can get the full sample project here.


One Common Exception

When you design a custom UICollectionViewListCell in an interface builder, one most common exception you might encounter is the ‘NSInternalInconsistencyException‘.

The exception message looks something like this in the Xcode console:

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', 
reason: 'Rounding frame ({{20, -8.9884656743115785e+307}, {374, 1.7976931348623157e+308}}) from preferred layout attributes resulted in a frame with one or more invalid members ({{20, -8.9884656743115785e+307}, {374, inf}}).
Layout attributes: <UICollectionViewLayoutAttributes: 0x7fd0d2043130> index path: (<NSIndexPath: 0x947fdb8ab9f8e625> {length = 2, path = 0 - 0}); frame = (20 -8.98847e+307; 374 1.79769e+308); zIndex = 10; 
View: <Swift_Senpai_UICollectionView_List.SFSymbolNameListCell: 0x7fd0cfc21470; baseClass = UICollectionViewListCell; frame = (20 17.5; 374 44); clipsToBounds = YES; layer = <CALayer: 0x600002768000>>'

This kind of exception is due to the auto layout engine not having sufficient information to determine the cell’s height. It is usually caused by missing vertical spacing constraints and missing height constraints within the content view. Therefore, when it happens, make sure to recheck the auto layout configurations of your content view’s content.


Wrapping Up

The concept presented in the “Creating Custom UIView in Interface Builder” section is basically connecting a UIView subclass to the interface builder. Therefore, this technique is not limited to just for creating a custom UICollectionViewListCell.

You can definitely use the same technique whenever you want to design a custom UIView using interface builder. If you are like me, prefer to use interface builder for UI creation, then I am sure that you will find this technique quite handy.

If you like this article, feel free to follow me on Twitter, and subscribe to my monthly newsletter.

Thanks for reading and happy designing. 👨🏻‍💻


Further Readings


👋🏻 Hey!

While you’re still here, why not check out some of my favorite Mac tools on Setapp? They will definitely help improve your day-to-day productivity. Additionally, doing so will also help support my work.

  • Bartender: Superpower your menu bar and take full control over your menu bar items.
  • CleanShot X: The best screen capture app I’ve ever used.
  • PixelSnap: Measure on-screen elements with ease and precision.
  • iStat Menus: Track CPU, GPU, sensors, and more, all in one convenient tool.