How to animate cell size change when using UIHostingConfiguration

I made an UICollectionView that uses a CompositionalLayout, DiffableDataSource, the new UIHostingConfiguration and an ObservableObject. You can resize cells by double tapping them (see gif + example code). The resizing is triggered by updating the ObservableObject.

Now I want to set an animation mainly to animate the shrinking as well as the size change of the last cell but I can't seem to find the right way to do so.

By default there is no animation when shrinking and on the last cell:

While there are many guides on how to create dynamically resizeable cells they all either use changes in Autolayout constraints (which can't be used with UIHostingConfiguration - correct me if I'm wrong) or they add an optional part to the view inside UIHostingConfiguration with "if some condition {MyAdditionalView(); .transition(someTransition)}" instead of changing its frame.

I also tried these different ways to manipulate the animation but with no success:

  1. setting an animation modifier .animation(): this makes a fading animation appear and the view inside the hosting configuration starts to jump (It seems to be anchored in the centre of the cell).
  2. setting a transition modifier .transition(): this has no effect on the animation.
  3. subclassing UICompositionalLayout and overriding initialLayoutAttributesForAppearingItem(at itemIndexPath: IndexPath) as well as finalLayoutAttributesForDisappearingItem(at itemIndexPath: IndexPath): this has no effect on the animation.
  4. subclassing UICollectionViewCell and overriding apply(_ layoutAttributes: UICollectionViewLayoutAttributes): this only effects the orange cell-background on initial appearance.
  5. using snapshot.reloadItems([ItemIdentifier]) or snapshot.reconfigureItems([ItemIdentifier]): they have no effect on the animation and lead to inconsistent behaviour when combined with ObservableObject.
  6. setting collectionView.selfSizingInvalidation = .enabledIncludingConstraints: no effect. Setting it to .disabled stops the orange cells from resizing altogether.

Is there a way to customize the size change animation of a cell using UIHostingConfiguration and ObservableObject?

Code without animation:

struct CellContentModel {
    var height: CGFloat? = 100
}

class CellContentController: ObservableObject, Identifiable {
    let id = UUID()

    @Published var cellContentModel: CellContentModel

    init(cellContentModel: CellContentModel) {
        self.cellContentModel = cellContentModel
    }

}

class DataStore {
    var data: [CellContentController]
    var dataById: [CellContentController.ID: CellContentController]

    init(data: [CellContentController]) {
        self.data = data
        self.dataById = Dictionary(uniqueKeysWithValues: data.map { ($0.id, $0) } )
    }

    static let testData = [
        CellContentController(cellContentModel: CellContentModel()),
        CellContentController(cellContentModel: CellContentModel(height: 80)),
        CellContentController(cellContentModel: CellContentModel())
    ]

}


class CollectionViewController: UIViewController {

    enum Section {
        case first
    }

    var dataStore = DataStore(data: DataStore.testData)

    private var layout: UICollectionViewCompositionalLayout!
    private var collectionView: UICollectionView!
    private var dataSource: UICollectionViewDiffableDataSource<Section, CellContentController.ID>!

    override func loadView() {
        createLayout()
        createCollectionView()
        createDataSource()
        view = collectionView
    }

}

// - MARK: Layout
extension CollectionViewController {
    func createLayout() {
        let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(50))

        let Item = NSCollectionLayoutItem(layoutSize: itemSize)

        let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.8), heightDimension: .estimated(300))

        let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [Item])

        let section = NSCollectionLayoutSection(group: group)

        layout = .init(section: section)
    }

}

// - MARK: CollectionView
extension CollectionViewController {
    func createCollectionView() {
        collectionView = .init(frame: .zero, collectionViewLayout: layout)

        let doubleTapGestureRecognizer = DoubleTapGestureRecognizer()
        doubleTapGestureRecognizer.doubleTapAction = { [unowned self] touch, _ in
            let touchLocation = touch.location(in: collectionView)

            guard let touchedIndexPath = collectionView.indexPathForItem(at: touchLocation) else { return }
            let touchedItemIdentifier = dataSource.itemIdentifier(for: touchedIndexPath)!

            dataStore.dataById[touchedItemIdentifier]!.cellContentModel.height = dataStore.dataById[touchedItemIdentifier]!.cellContentModel.height == 100 ? nil : 100 // <- this triggers the resizing
        }

        collectionView.addGestureRecognizer(doubleTapGestureRecognizer)
    }

}

// - MARK: DataSource
extension CollectionViewController {
    func createDataSource() {
        let cellRegistration = UICollectionView.CellRegistration<C, CellContentController.ID>() { cell, indexPath, itemIdentifier in
            let cellContentController = self.dataStore.dataById[itemIdentifier]!

            cell.contentConfiguration = UIHostingConfiguration {
                TextView(cellContentController: cellContentController)
            }
            .background(.orange)
        }

        dataSource = .init(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
            return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: itemIdentifier)
        }

        var initialSnapshot = NSDiffableDataSourceSnapshot<Section, CellContentController.ID>()
        initialSnapshot.appendSections([Section.first])
        initialSnapshot.appendItems(dataStore.data.map{ $0.id }, toSection: Section.first)
        dataSource.applySnapshotUsingReloadData(initialSnapshot)
    }

}

class DoubleTapGestureRecognizer: UITapGestureRecognizer {
    var doubleTapAction: ((UITouch, UIEvent) -> Void)?

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
        if touches.first!.tapCount == 2 {
            doubleTapAction?(touches.first!, event)
        }
    }

}

struct TextView: View {

    @StateObject var cellContentController: CellContentController

    var body: some View {
        Text(cellContentController.cellContentModel.height?.description ?? "nil")
            .frame(height: cellContentController.cellContentModel.height)
            .background(.green)
    }

}

I have improved my question and since you can't edit or delete I reposted it. You can find it here: Improved Question

How to animate cell size change when using UIHostingConfiguration
 
 
Q