Modern UI CollectionViews with snapshots - How to update cell content without completely reloading the cell?

547 views Asked by At

Duplication Allegations:

This question has wrongfully been flagged as a duplicate of another question I asked. This question here is about communicating changes TO the cell without entirely reloading it (to dynamically change its content instead of cross-fading to the new one), while the other is about structural ways of communicating information FROM the cell (to the view controller) as the cell's content is now abstracted away. That's two different things, thus this question is not a duplicate! Please reopen it. I do, however, realize that the question was not as clear as it should have been, which may have contributed to the fact so I've updated it.


iOS 14 introduced a new way to handle UICollectionViews and cells by using UIContentConfiguration, which holds the cell's data (source of truth). It's built so that the cell automatically refreshes whenever the data changes. But it refreshes the entire cell by cross-fading it to the new one. What if you want to have finer control over the update animations, e.g. when a name changes you want to only update that label, but not the entire cell?

Let's look at an example: You have a cell that shows the name of a person and that person's car. The data comes from a model Person and is loaded into the cell. The simplest way of passing the model to the cell would be via an ItemIdentifier, something like:

struct Person {
    var id: UUID
    var name: String
    var carName: String
}

...

enum Item {
    case person(Person)
}

This way, whenever the Person changes (e.g. the carName changes), you can apply a new snapshot to the data source and the cell is updated. This update, however, takes place as a cross-fade animation between the old and new versions of the cell. What if you didn't want that? What if you wanted the cell to only animate the car name, for example, without "animating" the entire cell (aka "let the cell animate itself to fit in the new data")? That's not possible with this approach.

So, in order to prevent Person changes from reloading entire cells, you have to adjust the ItemIdentifier to not include the entire Person struct but only its id or something:

enum Item {
    case person(UUID)
}

Now, as long as the id doesn't change, the cell isn't reloaded, which is perfect and gives the cell opportunity to update Person changes by itself. But how do you do that? Obviously, the cell now needs to grab the Person data from somewhere else but how to communicate changes of Person to that cell?

That's the question I'm trying to ask.

I recently posted a question related to this one. It's a "problem" I encountered while trying to solve the above question using Combine publishers to provide the cell with updates:

NSDiffableSnapshot causes collectionview's cell registration to be re-called even without any changes to the data

(To be clear, that question is also not a duplicate. It's an entirely different problem just motivated by the same thing!)

1

There are 1 answers

0
Raimundas Sakalauskas On

This is quite old, but since it doesn't have a confirmed answer I will give this a go.

You can loop over visible cells. The list shouldn't be that long and therefore the solution should be quite efficient.

func present(_ newState: State, animated: Bool = true) {
    self.state = newState

    // Hashes for all items haven't changed so only new cells will be shown
    // but none of the cells will get reloaded because of property change.
    var snapshot = Snapshot()
    snapshot.appendSections([0])
    snapshot.appendItems(state.allItems)
    dataSource.apply(snapshot, animatingDifferences: animated) { [weak self] in
        DispatchQueue.main.async {
            self?.updateTags()
        }
    }
}

func updateTags() {
    // This could also loop over visible cells only.
    for row in 0 ..< state.allItems.count {
        if let cell = v.tableView.cellForRow(at: IndexPath(row: row, section: 0)) as? TaskListCell {
            cell.tag = row
        }
    }
}