Jeffrey Wear
Jeffrey Wear

Reputation: 1374

How can a diffable collection view display the same item in multiple sections?

My app presents an emoji keyboard like Apple's where the emoji are displayed by-category. I render this using a collection view where the categories are sections. If an emoji has been inserted recently, it should appear in the "Frequently Used" category as well as whatever category it's normally in.

This is a problem for me trying to convert my collection view to use a UICollectionViewDiffableDataSource, because NSDiffableDataSourceSnapshot requires that items are unique. If I do something like

let snapshot = NSDiffableDataSourceSnapshot<Section, Emoji>()
snapshot.appendItems([thumbsUpEmoji], toSection: .frequents)
snapshot.appendItems([thumbsUpEmoji], toSection: .smileys)
dataSource.apply(snapshot)

I get warnings like

inserted identifier(s) already present; existing items will be moved into place for this current insertion. Please note this will impact performance if items are not unique when inserted.

And the emoji only shows up in one section, not both. How can I insert the item into multiple sections?

Upvotes: 7

Views: 3717

Answers (2)

Marchy
Marchy

Reputation: 3734

Here's a generic implementation so you can use this for all your sectioned lists:

/// The following allows items to be used across different shard key-spaces, by combining a shard's hash values with the item's hash.
///
/// EXAMPLE: For sectioned UICollectionViews, simply use a section identifier as the sharding key to allow items to show up in multiple sections-
struct ShardedItem<TShardingKey, TItem> : Hashable
    where TShardingKey:Hashable, TItem:Hashable
{
    let shard:TShardingKey
    let item:TItem
}

Then simply use it like so:

NOTE: Uses iOS 14 sectioned snapshots

var snapshot = NSDiffableDataSourceSnapshot<Section, ShardedItem<Section, Thing>>()
snapshot.appendSections( [.topThings, .allThings] )
self.dataSource.apply( snapshot, animatingDifferences: false )

let allThings:[Thing] = ...
let topThings:[Thing] = allThings.filter{ $0.isFeatured }
var topThingsSectionSnapshot = NSDiffableDataSourceSectionSnapshot<ShardedItem<Section, Thing>>()
topThingsSectionSnapshot.append( topThings.map { ShardedItem( shard: .topThings, item: $0 )})
self.dataSource.apply( topThingsSectionSnapshot, to: .topThings )

var allThingsSectionSnapshot = NSDiffableDataSourceSectionSnapshot<ShardedItem<MinorRegionsSection, MinorRegionListItem>>()
allThingsSectionSnapshot.append( allThings.map { ShardedItem( shard: .allThings, item: $0 )})
self.dataSource.apply( allThingsSectionSnapshot, to: .allThings )

Upvotes: 3

Jeffrey Wear
Jeffrey Wear

Reputation: 1374

I found that I was able to do this by wrapping the emoji in structs that associated the emoji to their sections:

struct CategorizedEmoji: Hashable {
    let emoji: Emoji
    let category: Section
}

My datasource is then of type UICollectionViewDiffableDataSource<Section, CategorizedEmoji> and the snapshots of type NSDiffableDataSourceSnapshot<Section, CategorizedEmoji>. When constructing the snapshot I do

let snapshot = NSDiffableDataSourceSnapshot<Section, CategorizedEmoji>()
snapshot.appendItems([CategorizedEmoji(emoji: thumbsUpEmoji, category: .frequents)], toSection: .frequents)
snapshot.appendItems([CategorizedEmoji(emoji: thumbsUpEmoji, category: .smileys)], toSection: .smileys)
dataSource.apply(snapshot)

A little verbose but really not too bad.

Caveat: I would guess that this solution would prevent the emoji from moving between sections (since an emoji in a different section would be represented by a completely different item). I personally don't need to handle that but I'd welcome seeing an answer that did figure that out.

Upvotes: 15

Related Questions