Skip to the content.

UICollectionView.CellRegistration, 그리고 iOS 15와 iOS 16

서론

iOS 15와 iOS 16에서 UICollectionView.CellRegistration의 일부 바뀐 점을 설명합니다. 우선 UIContentConfigurationUICollectionView.CellRegistration이 무엇인지 설명드려야 할 것 같습니다.

UIContentConfiguration이란

iOS 14 이후로 UIContentConfiguration (Swift / Obj-C)라는 protocol이 생겼습니다. UITableViewCellUICollectionViewCell을 커스텀 할 때 subclassing을 해야 할 필요가 없어지며, View 컴퍼넌트를 새로 하나 만들었을 경우 아래 코드처럼 UITableViewCellUICollectionViewCell에 손쉽게 주입할 수 있다는 장점이 있습니다.

let contentConfiguration: CustomContentConfiguration = /* */

let tableViewCell: UITableViewCell = /* */
tableViewCell.contentConfiguration = contentConfiguration

let collectionViewCell: UICollectionViewCell = /* */
collectionViewCell.contentConfiguration = contentConfiguration

이외에도 UIListSeparatorConfiguration, UIBackgroundConfiguration, UIButtonConfiguration 등이 있으나 이 글에서는 다루지 않겠습니다.

UICollectionView.CellRegistration이란

또한 iOS 14 이후에는 Cell을 등록(registration)하거나 재사용(Reuse) 새로운 방법이 생겼습니다. 기존에는 UICollectionView에 Cell을 등록 및 재사용을 위해 아래와 같이 했어야 했습니다.

let collectionView: UICollectionView = /* */

// 등록
collectionView.register(CustomCollectionViewCell.self, forCellWithReuseIdentifier: "cell")

// 재사용
let indexPath: IndexPath = /* */
let cell: CustomCollectionViewCell? = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as? CustomCollectionViewCell

Reuse Identifier를 써야 하고, dequeueReusableCell(withReuseIdentifier:for:)는 nullable이라서 귀찮다고 느끼셨을 겁니다. 이를 해결하기 위해 Reusable이라는 서드파티 라이브러리가 있긴 합니다.

이를 개선하기 위해 iOS 14에 UICollectionView.CellRegistration (Swift, Obj-C)라는 API가 추가됩니다. 위에서 언급한 코드를 아래처럼 개선할 수 있게 되며, 이러한 Cell Registration 코드를 컴퍼넌트 처럼 사용할 수 있다는 장점도 있습니다.

// 최초 한 번만 생성되어야 함 - 여러 번 생성될 경우 Cell 재사용이 안 되거나 크래시
// ItemModel은 Cell에 주입할 데이터
let cellRegistration: UICollectionView.CellRegistration<CustomCollectionViewListCell, ItemModel> = {
    .init { cell, indexPath, itemIdentifier in
        // CustomCollectionViewListCell.configure(with:)
        cell.configure(with: itemIdentifier)
    }
}

// Cell 얻어 오기
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell: UICollectionViewCell = collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: itemIdentifier)
    return cell
}

예제 코드

이제 위에서 언급한 UIContentConfigurationUICollectionView.CellRegistration이 iOS 15와 iOS 16에서 어떻게 바뀌었는지 설명하겠습니다.

설명을 위해 아래 예제 코드를 준비했습니다. iOS 14에서만 정상 작동하며, iOS 15 및 iOS 16에서는 크래시가 발동되는 코드입니다.

아래 예제 코드의 구조를 간단히 설명해 드리자면

import UIKit

// MARK: SectionModel

enum SectionModel: Int, Equatable, Hashable {
    case emojis, numbers
}


// MARK: ItemModel

enum ItemModel: Equatable, Hashable {
    case emoji(String), number(Int)
    
    static func ==(lhs: ItemModel, rhs: ItemModel) -> Bool {
        switch (lhs, rhs) {
        case let (.emoji(lhsEmoji), .emoji(rhsEmoji)):
            return lhsEmoji == rhsEmoji
        case let (.number(lhsNumber), .number(rhsNumber)):
            return lhsNumber == rhsNumber
        default:
            return false
        }
    }
    
    func hash(into hasher: inout Hasher) {
        switch self {
        case let .emoji(emoji):
            hasher.combine(emoji)
        case let .number(number):
            hasher.combine(number)
        }
    }
}


// MARK: - ViewModel

actor ViewModel {
    typealias DataSource = UICollectionViewDiffableDataSource<SectionModel, ItemModel>
    
    let dataSource: DataSource
    
    init(dataSource: DataSource) {
        self.dataSource = dataSource
    }
    
    func request() {
        var snapshot: NSDiffableDataSourceSnapshot<SectionModel, ItemModel> = .init()
        
        snapshot.appendSections([.emojis, .numbers])
        snapshot.appendItems(
            ["😀", "😙", "😝", "🧐", "😫", "😶‍🌫️", "🥶", "😰", "🤫", "🫡", "😑", "😬", "🫥", "😴", "🤢", "😈", "👿"].map { .emoji($0) },
            toSection: .emojis
        )
        snapshot.appendItems((0...100).map { .number($0) }, toSection: .numbers)
        
        dataSource.apply(snapshot, animatingDifferences: false)
    }
}


// - MARK: CollectionViewLayout

@MainActor
protocol CollectionViewLayoutDelegate: AnyObject {
    func collectionViewLayoutSectionModel(for section: Int) -> SectionModel?
}

@MainActor
final class CollectionViewLayout: UICollectionViewCompositionalLayout {
    convenience init(delegate: CollectionViewLayoutDelegate) {
        self.init { [weak delegate] section, environment -> NSCollectionLayoutSection? in
            guard let sectionModel: SectionModel = delegate?.collectionViewLayoutSectionModel(for: section) else {
                return nil
            }
            
            switch sectionModel {
            case .emojis:
                let itemSize: NSCollectionLayoutSize = .init(
                    widthDimension: .fractionalWidth(1.0),
                    heightDimension: .fractionalWidth(1.0)
                )
                
                let item: NSCollectionLayoutItem = .init(layoutSize: itemSize)
                item.contentInsets = .init(
                    top: 20.0,
                    leading: 20.0,
                    bottom: 20.0,
                    trailing: 20.0
                )
                
                let groupSize: NSCollectionLayoutSize = .init(
                    widthDimension: .absolute(100.0),
                    heightDimension: .absolute(100.0)
                )
                
                let group: NSCollectionLayoutGroup
                if #available(iOS 16.0, *) {
                    group = .horizontal(layoutSize: groupSize, repeatingSubitem: item, count: 1)
                } else {
                    group = .horizontal(layoutSize: groupSize, subitem: item, count: 1)
                }
                
                let layoutSection: NSCollectionLayoutSection = .init(group: group)
                layoutSection.orthogonalScrollingBehavior = .continuousGroupLeadingBoundary
                return layoutSection
            case .numbers:
                let layoutConfiguration: UICollectionLayoutListConfiguration = .init(appearance: .insetGrouped)
                let layoutSession: NSCollectionLayoutSection = .list(using: layoutConfiguration, layoutEnvironment: environment)
                return layoutSession
            }
        }
    }
}


// MARK: - EmojiContentConfiguration

struct EmojiContentConfiguration: UIContentConfiguration {
    let emoji: String
    
    @MainActor func makeContentView() -> UIView & UIContentView {
        let contentView: EmojiContentView = .init(frame: .null)
        contentView.configuration = self
        return contentView
    }
    
    func updated(for state: UIConfigurationState) -> EmojiContentConfiguration {
        return self
    }
}


// MARK: - EmojiContentView

@MainActor
final class EmojiContentView: UIView {
    private var _configuration: EmojiContentConfiguration! {
        didSet {
            label.text = _configuration.emoji
        }
    }
    private let label: UILabel = .init(frame: .null)
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        
        backgroundColor = .systemBackground
        
        //
        
        label.textAlignment = .center
        addSubview(label)
        label.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            label.topAnchor.constraint(equalTo: topAnchor),
            label.leadingAnchor.constraint(equalTo: leadingAnchor),
            label.trailingAnchor.constraint(equalTo: trailingAnchor),
            label.bottomAnchor.constraint(equalTo: bottomAnchor)
        ])
    }
    
    @available(*, unavailable)
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

extension EmojiContentView: UIContentView {
    var configuration: UIContentConfiguration {
        get {
            return _configuration
        }
        set(newValue) {
            _configuration = newValue as? EmojiContentConfiguration
        }
    }
    
    @available(iOS 16.0, *)
    func supports(_ configuration: UIContentConfiguration) -> Bool {
        configuration is EmojiContentConfiguration
    }
}


// MARK: - ViewController

@MainActor
final class ViewController: UIViewController {
    private var collectionView: UICollectionView!
    private var viewModel: ViewModel!
    private var requestionTask: Task<Void, Never>?
    
    private var cellRegistration: UICollectionView.CellRegistration<UICollectionViewListCell, ItemModel> {
        .init { cell, indexPath, itemIdentifier in
            switch itemIdentifier {
            case let .emoji(emoji):
                let configuration: EmojiContentConfiguration = .init(emoji: emoji)
                cell.contentConfiguration = configuration
            case let .number(number):
                var configuration: UIListContentConfiguration = cell.defaultContentConfiguration()
                configuration.text = "\(number)"
                cell.contentConfiguration = configuration
            }
        }
    }
    
    deinit {
        requestionTask?.cancel()
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        //
        
        let collectionViewLayout: CollectionViewLayout = .init(delegate: self)
        let collectionView: UICollectionView = .init(frame: .null, collectionViewLayout: collectionViewLayout)
        
        collectionView.backgroundColor = .systemGroupedBackground
        view.addSubview(collectionView)
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            collectionView.topAnchor.constraint(equalTo: view.topAnchor),
            collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
        
        //
        
        let dataSource: ViewModel.DataSource = .init(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
            let cellRegistration: UICollectionView.CellRegistration<UICollectionViewListCell, ItemModel> = self.cellRegistration
            let cell: UICollectionViewCell = collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: itemIdentifier)
            
            return cell
        }
        let viewModel: ViewModel = .init(dataSource: dataSource)
        
        let requestionTask: Task<Void, Never> = .detached { [viewModel] in
            await viewModel.request()
        }
        
        //
        
        self.collectionView = collectionView
        self.viewModel = viewModel
        self.requestionTask = requestionTask
    }
}

extension ViewController: CollectionViewLayoutDelegate {
    func collectionViewLayoutSectionModel(for section: Int) -> SectionModel? {
        if #available(iOS 15.0, *) {
            return viewModel.dataSource.sectionIdentifier(for: section)
        } else {
            return viewModel.dataSource.snapshot().sectionIdentifiers[section]
        }
    }
}

iOS 15

위 코드는 iOS 15 이상에서 아래처럼 Exception이 발생합니다.

2022-10-08 18:35:09.630295+0900 MyAppSwift[976:204942] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Attempted to dequeue a cell using a registration that was created inside -collectionView:cellForItemAtIndexPath: or inside a UICollectionViewDiffableDataSource cell provider. Creating a new registration each time a cell is requested will prevent reuse and cause created cells to remain inaccessible in memory for the lifetime of the collection view. Registrations should be created up front and reused. Registration: <UICollectionViewCellRegistration: 0x2807fc870>'

Cell을 재생성 할 때마다 registration을 만들지 말라는 소리같네요. 아래 부분이 문제 같습니다.

let dataSource: ViewModel.DataSource = .init(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
    let cellRegistration: UICollectionView.CellRegistration<UICollectionViewListCell, ItemModel> = self.cellRegistration
    let cell: UICollectionViewCell = collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: itemIdentifier)
    
    return cell
}

위 코드를 아래처럼 수정하면 해결이 됩니다. registration을 최초 한 번만 생성하면 됩니다.

let cellRegistration: UICollectionView.CellRegistration<UICollectionViewListCell, ItemModel> = self.cellRegistration
    
let dataSource: ViewModel.DataSource = .init(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
    let cell: UICollectionViewCell = collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: itemIdentifier)
    
    return cell
}

이는 Documentation에서도 명시되어 있는 부분입니다. 링크

Don’t create your cell registration inside a UICollectionViewDiffableDataSource.CellProvider closure; doing so prevents cell reuse, and generates an exception in iOS 15 and higher.

iOS 16

위에서 설명드린 iOS 15 코드를 적용하면 Exception은 발생하지 않으나, UICollectionView를 스크롤해서 Cell 재사용을 발동시키면 아래처럼 Warning이 발생합니다.

2022-10-08 18:39:58.908812+0900 MyAppSwift[982:208779] [ContentConfiguration] Warning: You are setting a new content configuration to a cell that has an existing content configuration, but the existing content view does not support the new configuration. This means the existing content view must be replaced with a new content view created from the new configuration, instead of updating the existing content view directly, which is expensive. Use separate registrations or reuse identifiers for different types of cells to avoid this. Make a symbolic breakpoint at UIContentConfigurationAlertForReplacedContentView to catch this in the debugger.
Cell: <UICollectionViewListCell: 0x10521b680; frame = (420 20; 60 60); layer = <CALayer: 0x2826c06a0>>;
Existing content configuration: <UIListContentConfiguration: 0x280dfc7e0; text = "31"; Base Style = Cell; directionalLayoutMargins = {11, 16, 11, 8}; axesPreservingSuperviewLayoutMargins = [Horizontal]; imageToTextPadding = 16; textToSecondaryTextVerticalPadding = 3>;
New content configuration: EmojiContentConfiguration(emoji: "😫")

하나의 Cell에 서로 다른 타입의 UIContentConfiguration을 적용하지 말라는 것 같습니다. 제가 작성했던 코드를 보시면 그렇게 하고 있습니다.

private var cellRegistration: UICollectionView.CellRegistration<UICollectionViewListCell, ItemModel> {
    .init { cell, indexPath, itemIdentifier in
        switch itemIdentifier {
        case let .emoji(emoji):
            let configuration: EmojiContentConfiguration = .init(emoji: emoji)
            cell.contentConfiguration = configuration
        case let .number(number):
            var configuration: UIListContentConfiguration = cell.defaultContentConfiguration()
            configuration.text = "\(number)"
            cell.contentConfiguration = configuration
        }
    }
}

이를 defaultCellRegistrationemojiCellRegistration로 쪼갭시다.

private var defaultCellRegistration: UICollectionView.CellRegistration<UICollectionViewListCell, String> {
    .init { cell, indexPath, itemIdentifier in
        var configuration: UIListContentConfiguration = cell.defaultContentConfiguration()
        configuration.text = itemIdentifier
        cell.contentConfiguration = configuration
    }
}

private var emojiCellRegistration: UICollectionView.CellRegistration<UICollectionViewCell, String> {
    .init { cell, indexPath, itemIdentifier in
        let configuration: EmojiContentConfiguration = .init(emoji: itemIdentifier)
        cell.contentConfiguration = configuration
    }
}

또한 Cell을 재사용하는 코드에서 Registration 분기 처리가 필요해 보입니다.

let defaultCellRegistration: UICollectionView.CellRegistration<UICollectionViewListCell, String> = self.defaultCellRegistration
let emojiCellRegistration: UICollectionView.CellRegistration<UICollectionViewCell, String> = self.emojiCellRegistration
let dataSource: ViewModel.DataSource = .init(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
    
    let cell: UICollectionViewCell
    
    switch itemIdentifier {
    case let .emoji(emoji):
        cell = collectionView.dequeueConfiguredReusableCell(using: emojiCellRegistration, for: indexPath, item: emoji)
    case let .number(number):
        cell = collectionView.dequeueConfiguredReusableCell(using: defaultCellRegistration, for: indexPath, item: "\(number)")
    }
    
    return cell
}

이렇게 하니 iOS 16에서 문제가 없네요.

총평

iOS 14 때 나온 API들인데 애플은 이런 중요한 내용들을 왜 이제야 고지하는 것인가… -_-

혹시 제가 또 놓친 것이 있어서 iOS 17에서는 작동하지 않을 수도 있어요… ㅎㅎ;

완성된 코드

import UIKit

// MARK: SectionModel

enum SectionModel: Int, Equatable, Hashable {
    case emojis, numbers
}


// MARK: ItemModel

enum ItemModel: Equatable, Hashable {
    case emoji(String), number(Int)
    
    static func ==(lhs: ItemModel, rhs: ItemModel) -> Bool {
        switch (lhs, rhs) {
        case let (.emoji(lhsEmoji), .emoji(rhsEmoji)):
            return lhsEmoji == rhsEmoji
        case let (.number(lhsNumber), .number(rhsNumber)):
            return lhsNumber == rhsNumber
        default:
            return false
        }
    }
    
    func hash(into hasher: inout Hasher) {
        switch self {
        case let .emoji(emoji):
            hasher.combine(emoji)
        case let .number(number):
            hasher.combine(number)
        }
    }
}


// MARK: - ViewModel

actor ViewModel {
    typealias DataSource = UICollectionViewDiffableDataSource<SectionModel, ItemModel>
    
    let dataSource: DataSource
    
    init(dataSource: DataSource) {
        self.dataSource = dataSource
    }
    
    func request() {
        var snapshot: NSDiffableDataSourceSnapshot<SectionModel, ItemModel> = .init()
        
        snapshot.appendSections([.emojis, .numbers])
        snapshot.appendItems(
            ["😀", "😙", "😝", "🧐", "😫", "😶‍🌫️", "🥶", "😰", "🤫", "🫡", "😑", "😬", "🫥", "😴", "🤢", "😈", "👿"].map { .emoji($0) },
            toSection: .emojis
        )
        snapshot.appendItems((0...100).map { .number($0) }, toSection: .numbers)
        
        dataSource.apply(snapshot, animatingDifferences: false)
    }
}


// - MARK: CollectionViewLayout

@MainActor
protocol CollectionViewLayoutDelegate: AnyObject {
    func collectionViewLayoutSectionModel(for section: Int) -> SectionModel?
}

@MainActor
final class CollectionViewLayout: UICollectionViewCompositionalLayout {
    convenience init(delegate: CollectionViewLayoutDelegate) {
        self.init { [weak delegate] section, environment -> NSCollectionLayoutSection? in
            guard let sectionModel: SectionModel = delegate?.collectionViewLayoutSectionModel(for: section) else {
                return nil
            }
            
            switch sectionModel {
            case .emojis:
                let itemSize: NSCollectionLayoutSize = .init(
                    widthDimension: .fractionalWidth(1.0),
                    heightDimension: .fractionalWidth(1.0)
                )
                
                let item: NSCollectionLayoutItem = .init(layoutSize: itemSize)
                item.contentInsets = .init(
                    top: 20.0,
                    leading: 20.0,
                    bottom: 20.0,
                    trailing: 20.0
                )
                
                let groupSize: NSCollectionLayoutSize = .init(
                    widthDimension: .absolute(100.0),
                    heightDimension: .absolute(100.0)
                )
                
                let group: NSCollectionLayoutGroup
                if #available(iOS 16.0, *) {
                    group = .horizontal(layoutSize: groupSize, repeatingSubitem: item, count: 1)
                } else {
                    group = .horizontal(layoutSize: groupSize, subitem: item, count: 1)
                }
                
                let layoutSection: NSCollectionLayoutSection = .init(group: group)
                layoutSection.orthogonalScrollingBehavior = .continuousGroupLeadingBoundary
                return layoutSection
            case .numbers:
                let layoutConfiguration: UICollectionLayoutListConfiguration = .init(appearance: .insetGrouped)
                let layoutSession: NSCollectionLayoutSection = .list(using: layoutConfiguration, layoutEnvironment: environment)
                return layoutSession
            }
        }
    }
}


// MARK: - EmojiContentConfiguration

struct EmojiContentConfiguration: UIContentConfiguration {
    let emoji: String
    
    @MainActor func makeContentView() -> UIView & UIContentView {
        let contentView: EmojiContentView = .init(frame: .null)
        contentView.configuration = self
        return contentView
    }
    
    func updated(for state: UIConfigurationState) -> EmojiContentConfiguration {
        return self
    }
}


// MARK: - EmojiContentView

@MainActor
final class EmojiContentView: UIView {
    private var _configuration: EmojiContentConfiguration! {
        didSet {
            label.text = _configuration.emoji
        }
    }
    private let label: UILabel = .init(frame: .null)
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        
        backgroundColor = .systemBackground
        
        //
        
        label.textAlignment = .center
        addSubview(label)
        label.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            label.topAnchor.constraint(equalTo: topAnchor),
            label.leadingAnchor.constraint(equalTo: leadingAnchor),
            label.trailingAnchor.constraint(equalTo: trailingAnchor),
            label.bottomAnchor.constraint(equalTo: bottomAnchor)
        ])
    }
    
    @available(*, unavailable)
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

extension EmojiContentView: UIContentView {
    var configuration: UIContentConfiguration {
        get {
            return _configuration
        }
        set(newValue) {
            _configuration = newValue as? EmojiContentConfiguration
        }
    }
    
    @available(iOS 16.0, *)
    func supports(_ configuration: UIContentConfiguration) -> Bool {
        configuration is EmojiContentConfiguration
    }
}


// MARK: - ViewController

@MainActor
final class ViewController: UIViewController {
    private var collectionView: UICollectionView!
    private var viewModel: ViewModel!
    private var requestionTask: Task<Void, Never>?
    
    private var defaultCellRegistration: UICollectionView.CellRegistration<UICollectionViewListCell, String> {
        .init { cell, indexPath, itemIdentifier in
            var configuration: UIListContentConfiguration = cell.defaultContentConfiguration()
            configuration.text = itemIdentifier
            cell.contentConfiguration = configuration
        }
    }
    
    private var emojiCellRegistration: UICollectionView.CellRegistration<UICollectionViewCell, String> {
        .init { cell, indexPath, itemIdentifier in
            let configuration: EmojiContentConfiguration = .init(emoji: itemIdentifier)
            cell.contentConfiguration = configuration
        }
    }
    
    deinit {
        requestionTask?.cancel()
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        //
        
        let collectionViewLayout: CollectionViewLayout = .init(delegate: self)
        let collectionView: UICollectionView = .init(frame: .null, collectionViewLayout: collectionViewLayout)
        
        collectionView.backgroundColor = .systemGroupedBackground
        view.addSubview(collectionView)
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            collectionView.topAnchor.constraint(equalTo: view.topAnchor),
            collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
        
        //
        
        let defaultCellRegistration: UICollectionView.CellRegistration<UICollectionViewListCell, String> = self.defaultCellRegistration
        let emojiCellRegistration: UICollectionView.CellRegistration<UICollectionViewCell, String> = self.emojiCellRegistration
        let dataSource: ViewModel.DataSource = .init(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
            
            let cell: UICollectionViewCell
            
            switch itemIdentifier {
            case let .emoji(emoji):
                cell = collectionView.dequeueConfiguredReusableCell(using: emojiCellRegistration, for: indexPath, item: emoji)
            case let .number(number):
                cell = collectionView.dequeueConfiguredReusableCell(using: defaultCellRegistration, for: indexPath, item: "\(number)")
            }
            
            return cell
        }
        let viewModel: ViewModel = .init(dataSource: dataSource)
        
        let requestionTask: Task<Void, Never> = .detached { [viewModel] in
            await viewModel.request()
        }
        
        //
        
        self.collectionView = collectionView
        self.viewModel = viewModel
        self.requestionTask = requestionTask
    }
}

extension ViewController: CollectionViewLayoutDelegate {
    func collectionViewLayoutSectionModel(for section: Int) -> SectionModel? {
        if #available(iOS 15.0, *) {
            return viewModel.dataSource.sectionIdentifier(for: section)
        } else {
            return viewModel.dataSource.snapshot().sectionIdentifiers[section]
        }
    }
}