What are the best practices for managing custom properties with cells configurations?

I'm not sure if I need to store any variable used in updateConfiguration(using:) in the custom key value store of UICellConfigurationState.

I'm skeptical because some of these variables do not semantically represent a "state".

If I look at the sample WWDC sample code for custom cells configurations, all the variables used in updateConfiguration(using:) method are stored in the custom key value store of UICellConfigurationState. In fact, in this sample code, it's the whole model that it's stored in a state custom key.

It seems weird because the base properties of UICellConfigurationState are real "state" properties: like selected, highlighted.

In the same way, for the cells provided by UIKit, non state properties are stored in the configuration. For example, UIListContentConfiguration.imageProperties.tintColor, UIListContentConfiguration.imageToTextPadding.

For a custom cell, we could imagine to subclass UIListContentConfiguration but it's not possible because it's a struct.

Imagine you have a custom UICollectionViewListCell with a custom checkmark view.

Associated to this checkmark, you have:
  • checkmarkTintColor: an “appearance” property to set the tint color of the checkmark.

  • checkmarkChecked: a ”state” property associated to the checkmark state.

What is the good practice? Storing them the state key value store like this?



Code Block
var checkmarkChecked: Bool = false {
didSet {
guard oldValue != checked else {
return
}
setNeedsUpdateConfiguration()
}
}
var checkmarkTintColor: UIColor? = nil {
didSet {
guard oldValue != checkmarkTintColor else {
return
}
setNeedsUpdateConfiguration()
}
}
var configuration: UIListContentConfiguration? {
didSet {
guard oldValue != configuration else {
return
}
setNeedsUpdateConfiguration()
}
}
override var configurationState: UICellConfigurationState {
var state = super.configurationState
state.checkmarkChecked = checkmarkChecked
state.checkmarkTintColor = checkmarkTintColor
return state
}
override func updateConfiguration(using state: UICellConfigurationState) {
...
checkmarkImageView.image = state.checked ? UIImage(systemName: "checkmark.circle.fill") : UIImage(systemName: "circle")
checkmarkImageView.tintColor = checkmarkTintColor
}

Answered by Frameworks Engineer in 628576022

From what I understand, if I make a custom cell, I cannot have a custom configuration. Let's take an example:

If I make a custom UICollectionViewListCell cell for a sidebar, then use UIListContentConfiguration.sidebarCell() in order to have all its rendering for free (text label, image view, ...). I would insert the UIListContentView associated to UIListContentConfiguration.sidebarCell() in the view hiearchy and add some custom views.

If I have some properties controlling the appearance for these extra custom views, they should be placed in a configuration but since my custom views are in the cell's context, it's not possible because I don't have any configuration associated to them.

So, in this case, I think it's a limitation of the API and I need to place these properties in the UICellConfigurationStatecustom key value store.

There are a couple different ways to interpret what custom cell means:
  • A subclass of UICollectionViewCell or UICollectionViewListCell that you create. This is very common. You can use a cell subclass without manually adding any custom views to its contentView, if you set the contentConfiguration of the cell directly.

  • A cell with your own custom views that you create and add to the cell's contentView. Usually this is implemented in a cell subclass.

If you want to add your own custom views to the cell's contentView, then you don't want to set the contentConfiguration property on the cell. (Setting the contentConfiguration will replace any existing contentView with the content view created from the configuration.) Instead, if you are adding custom views and also want to use a content configuration, that's when you create a UIListContentView manually, and then add that to the cell's contentView alongside your other custom views.

Now, in addition to all that, you can also create a custom content configuration and content view, using the UIContentConfiguration and UIContentView protocols. This allows you to create a configuration type (like UIListContentConfiguration) and a paired content view type (like UIListContentView), and then use that custom configuration just like a system-provided one. You can set a custom configuration to the contentConfiguration property directly on the cell, and provided you implemented it correctly, it will create or update your content view and allow you to do anything you want.

If you are making a custom content configuration and view, you can use composition to build on top of the system-provided UIListContentConfiguration and UIListContentView. Just include a UIListContentConfiguration as a property on your custom configuration, and then in your custom content view implementation, create a UIListContentView that you add and update using the UIListContentConfiguration from your custom configuration. In this way, you can use all the features of the system-provided configuration/view, and add additional views or behaviors on top of it.

To answer your question, if you are just adding a UIListContentView and one or two custom views alongside it to the cell's contentView, and are not using a custom content configuration, you don't need to put the properties for your extra custom views in a content configuration, but you still may want to put those properties into the UICellConfigurationState so you can get some of the benefits discussed earlier. For example, you can do all the setup and updating of your extra custom views from inside your cell subclass' override of updateConfiguration(using state:), based entirely on the state passed in. See the CustomListCell in the Implementing Modern Collection Views sample code for an example of this. You don't have to put those properties into the UICellConfigurationState if you don't want to, though — you could manage your cell's custom views the same way you would have before iOS 14, if you prefer.
Think of UICellConfigurationState as a collection of all the inputs that affect how you would configure your cell. In other words, the properties on UICellConfigurationState represent "what it is" (things like whether it is disabled or selected, traits such as the idiom and content size category, etc) — these are all properties of the cell and its environment at any point in time. Note that this doesn't include anything about "how it looks" (the exact styling or rendering being used) — that's what a background or content configuration is, and you generate one of those configurations for a specific UICellConfigurationState.

So UICellConfigurationState isn't just for interaction states (highlighted, selected, focused, reordering, etc). UIKit provides those properties because those are common generally-applicable states, and the default system-provided configurations return appearance customizations for many of those states.

You can use custom states to add any additional data you want to UICellConfigurationState. These can be simple boolean states specific to your app, e.g. a task manager app might have isCompleted and isOverdue properties for each task cell. However, if you use a view model to set up your cell (e.g. a struct that contains the formatted text, images, and other properties relevant to what the cell displays), that view model is an important direct input into the configuration of your cell, and it makes sense to incorporate that into the configuration state. (Note that while a lightweight view model tailored specifically to the needs of the cell would make sense, you almost certainly don't want to add an entire lower-level data layer model object as a custom state. As a best practice, views should never own or depend on your data layer model objects directly; generate a lightweight view model from your model object based on what you actually need to display.)

Ultimately the decision of what you use UICellConfigurationState for is up to you. Here are some of the benefits of using it to encapsulate all of the data and inputs that you use to build your configurations:

Anytime any of the inputs change that might affect the configuration of the cell, you just need to call setNeedsUpdateConfiguration(), and if there are multiple changes they will get coalesced into a single update automatically for best performance.

The code that builds your content and background configurations can be written as a pure function of the UICellConfigurationState, meaning it does not rely on any other inputs and does not produce any side effects. For example, you could write a static function like this, which takes a UICellConfigurationState and returns a pair of background & content configurations:
Code Block swift
static func configurations(for state: UICellConfigurationState) -> (UIBackgroundConfiguration, UIContentConfiguration)

This pattern makes your code simpler to reason about, easier to debug, and makes it testable by default. It also makes it much easier to refactor and share in more places, as your implementation no longer depends on the cell class it gets used with, or any specific instance at all.

UICellConfigurationState can serve as a "common currency" that can be passed and shared across multiple parts of your code as needed. For example, your app might have custom states that are shared by many different cell classes, and then have a single shared styling component that can return colors (or entire configurations!) for different UICellConfigurationState values passed to it from inside each cell class' implementation of updateConfiguration(using state:).

Finally, if you create your own custom content configurations using the UIContentConfiguration protocol, custom states allow your configuration to support automatic updates and return an updated version of itself for new states from updated(for:) (documentation). This allows your custom content configuration to change its appearance based on your custom states, without having to write any code in your cell subclass to manually set properties on the configuration.



To answer your specific questions about the checkmark view properties:
  • checkmarkChecked

    • This is a great candidate to have as a custom state in UICellConfigurationState.

  • checkmarkTintColor

    • This may or may not make sense to model as a custom state. Usually, colors aren't an input into the cell configuration, they are an output based on the configuration state. If you'd change this color based on whether the cell is selected or not, for example, it probably doesn't belong in the configuration state itself, and if you were implementing a custom content configuration, this would be a property on that configuration instead. However, if you think of this color as a property of the cell or the item that the cell represents (e.g. perhaps it is a user-configurable color for that item, like how you can choose to assign a color to a file or folder in Finder on the Mac), then that color may make sense as part of your view model and/or UICellConfigurationState.

Thank you for taking the time to answer my question clearly. It helped!

I still have some hesitation about the choice: custom configurations versus custom cells.

From what I understand, if I make a custom cell, I cannot have a custom configuration. Let's take an example:

If I make a custom UICollectionViewListCell cell for a sidebar, then use UIListContentConfiguration.sidebarCell() in order to have all its rendering for free (text label, image view, ...). I would insert the UIListContentView associated to UIListContentConfiguration.sidebarCell() in the view hiearchy and add some custom views.

If I have some properties controlling the appearance for these extra custom views, they should be placed in a configuration but since my custom views are in the cell's context, it's not possible because I don't have any configuration associated to them.

So, in this case, I think it's a limitation of the API and I need to place these properties in the UICellConfigurationState custom key value store.
Accepted Answer

From what I understand, if I make a custom cell, I cannot have a custom configuration. Let's take an example:

If I make a custom UICollectionViewListCell cell for a sidebar, then use UIListContentConfiguration.sidebarCell() in order to have all its rendering for free (text label, image view, ...). I would insert the UIListContentView associated to UIListContentConfiguration.sidebarCell() in the view hiearchy and add some custom views.

If I have some properties controlling the appearance for these extra custom views, they should be placed in a configuration but since my custom views are in the cell's context, it's not possible because I don't have any configuration associated to them.

So, in this case, I think it's a limitation of the API and I need to place these properties in the UICellConfigurationStatecustom key value store.

There are a couple different ways to interpret what custom cell means:
  • A subclass of UICollectionViewCell or UICollectionViewListCell that you create. This is very common. You can use a cell subclass without manually adding any custom views to its contentView, if you set the contentConfiguration of the cell directly.

  • A cell with your own custom views that you create and add to the cell's contentView. Usually this is implemented in a cell subclass.

If you want to add your own custom views to the cell's contentView, then you don't want to set the contentConfiguration property on the cell. (Setting the contentConfiguration will replace any existing contentView with the content view created from the configuration.) Instead, if you are adding custom views and also want to use a content configuration, that's when you create a UIListContentView manually, and then add that to the cell's contentView alongside your other custom views.

Now, in addition to all that, you can also create a custom content configuration and content view, using the UIContentConfiguration and UIContentView protocols. This allows you to create a configuration type (like UIListContentConfiguration) and a paired content view type (like UIListContentView), and then use that custom configuration just like a system-provided one. You can set a custom configuration to the contentConfiguration property directly on the cell, and provided you implemented it correctly, it will create or update your content view and allow you to do anything you want.

If you are making a custom content configuration and view, you can use composition to build on top of the system-provided UIListContentConfiguration and UIListContentView. Just include a UIListContentConfiguration as a property on your custom configuration, and then in your custom content view implementation, create a UIListContentView that you add and update using the UIListContentConfiguration from your custom configuration. In this way, you can use all the features of the system-provided configuration/view, and add additional views or behaviors on top of it.

To answer your question, if you are just adding a UIListContentView and one or two custom views alongside it to the cell's contentView, and are not using a custom content configuration, you don't need to put the properties for your extra custom views in a content configuration, but you still may want to put those properties into the UICellConfigurationState so you can get some of the benefits discussed earlier. For example, you can do all the setup and updating of your extra custom views from inside your cell subclass' override of updateConfiguration(using state:), based entirely on the state passed in. See the CustomListCell in the Implementing Modern Collection Views sample code for an example of this. You don't have to put those properties into the UICellConfigurationState if you don't want to, though — you could manage your cell's custom views the same way you would have before iOS 14, if you prefer.
Excellent, you answer clarifies all the patterns we can use for different kinds of custom cells. Thank you again.
I know this is an older thread but Apple really needs to rethink this API, it's very very messy, the sample code doesn't match what's covered in the WWDC video, and it's absolutely infuriating to work with if you want any kind of customization. I shouldn't have to create a diagram based on the comments above (which are way more helpful than the docs or WWDC video) just to get a basic custom cell to work. Yikes.


What are the best practices for managing custom properties with cells configurations?
 
 
Q