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:
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:
- 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. - Call the
loadNib()
function during initialization to configureSFSymbolNameContentView
accordingly. - Within the
loadNib()
function, load a nib file namedSFSymbolNameContentView.xib
and makeself
as the owner of the nib file. - Make the container view take up the entire content view using auto layout constraints.
- 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:
- Create a new user interface view.
- Set the file’s owner’s class.
- Resize the custom view.
- Add a label and configure auto layout.
- Connecting all
IBOutlet
s.
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
.
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
.
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“.
After that, open the size inspector and set the view 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.
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:
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! 😨
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.
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.
“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.
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
- UICollectionView List with Custom Cell and Custom Configuration
- Building an Expandable List Using UICollectionView: Part 1
- Building an Expandable List Using UICollectionView: Part 2
- Declarative UICollectionView List Header and Footer
- UICollectionView List with Interactive Custom Header
- Replicate the Expandable Date Picker Using UICollectionView List
👋🏻 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.