Create CollectionView in a similar way to new DataSource introdeced in iOS 13
The demo is based on Apple's ImplementingModernCollectionViews.
UICollectionViewDiffableDataSource: iOS 13 required
ConfigableCollectionView: iOS 9 required, but technically supporting all iOS versions since iOS 6
UICollectionViewDiffableDataSource: once you append a repeated object, it crashes, even in release, and it doesn't someTimes in some iOS versions
ConfigableCollectionView : only assert during debugging when you append a repetitive object
No distinction between NSDiffableDataSourceSectionSnapshot and NSDiffableDataSourceSnapshot
The touch to cell is based on hittest rather than cell.bounds(this is how UICollectionView work), so you can override the view’s hittest you use to make sure your tap action works properly.
UICollectionViewDiffableDataSource : only supports one Section type and one Item type
ConfigableCollectionView: multiple items and sections supported!
UICollectionViewDiffableDataSource has a bug( or is it a feature? ), and when you are using a class as ItemType, it won't hash the item with hashValue rather it's address.
let collectionView = CollectionView<Section, Item>(layout: generateLayout())
let collectionView = CollectionView<Any, Any>(layout: generateLayout())
collectionView.register(
view: { // create view for reuse
UICollectionViewListCell()
},
.config { // config the UICollectionViewListCell with Item
let cell = $0.view
let item = $0.data
var contentConfiguration = cell.defaultContentConfiguration()
contentConfiguration.text = item.title
contentConfiguration.textProperties.font = .preferredFont(forTextStyle: .headline)
cell.contentConfiguration = contentConfiguration
let disclosureOptions = UICellAccessory.OutlineDisclosureOptions(style: .header)
cell.accessories = [.outlineDisclosure(options:disclosureOptions)]
cell.backgroundConfiguration = UIBackgroundConfiguration.clear()
},
.when { // Optional, deciding when to use this type of view if need
!$0.data.subitems.isEmpty
}
)
collectionView.register(
view { // create normal view for reuse
ContentView()
},
.config(map: \.title) { // config the ContentView with Item.title, configurationState: UICellConfigurationState(introduced in iOS 14, no useful when you using UICollectionViewCell as View)
$0.view.data = $0.data
if $0.configurationState.isHighlighted {
$0.view.backgroundColor = .red
}
...
},
.flowLayoutSize { _ in // Optional, deciding the size in flow layout
CGSize(width: 100, height: 100)
},
.tap { _ in // Optional, deciding what to do after tap the view
Router.push( ... )
}
)
...
collectionView.register(
dataType: Int.self,
view {
ContentView()
},
.config {
$0.view.data = $0.data
}
)
collectionView.register(
dataType: String.self,
view {
UILabel()
},
.config {
$0.view.text = $0.data
}
)
The view closure is a ViewBuilder that supports weak reference of object w hen config view once the view is created, like
collectionView.register(
dataType: Int.self,
view { [weak self] in
if let color = self?.color {
ContentView(color: color)
}
},
.config {
$0.view.data = $0.data
}
)
Attention: if using subClass of UICollectionViewCell, it won't use the view closure to create the view, rather than using UICollectionView.dequeue.
like:
collectionView.dataManager.appendSections([Section.main])
collectionView.dataManager.appendItems(mountains)
or just
collectionView.dataManager.applyItems(mountains, updatedSection: Section.main)
or:
support recursivePath in appendChildItems
collectionView.dataManager.appendChildItems(menuItems, to: nil, recursivePath: \.subitems)
is equal to:
func addItems(_ menuItems: [OutlineItem], to parent: OutlineItem?) {
collectionView.dataManager.appendChildItems(menuItems, to: parent)
for menuItem in menuItems where !menuItem.subitems.isEmpty {
addItems(menuItem.subitems, to: menuItem)
}
}
addItems(menuItems, to: nil)
or:
use multi type of Item in the same section
let numbers: [Int]
let stings: [String]
collectionView.dataManager.appendItems(numbers)
collectionView.dataManager.appendItems(stings)
collectionView.register(
dataType: Int.self,
view {
NumberView()
}
)
collectionView.register(
dataType: String.self,
view {
UILabel()
}
)
Animating & update completion callback, using .on(animatingDifferences: completion) after all data handling functions, like:
collectionView.dataManager.appendItems(stings)
.on(animatingDifferences: false, completion: { print("appended") })
For more on usage, check out the difference in ImplementingModernCollectionViews.
To support the old iOS version, it is using NSDiffableDataSourceSnapshot above iOS 13, and using the data directly into a CustomUICollectionViewDataSource below iOS 13, to reduce the count of recreated NSDiffableDataSourceSnapshot instances, so the reload cells of CollectionView are async. To avoid that you can call the reloadImmediately()
You can use your own UICollectionViewDelegate(some delegate functions won't call), but you can't reset the UICollectionViewDataSource.
Known Issues:
The filter on appending children is not achieved due to performance issues, so it still adds to NSDiffableDataSourceSectionSnapshot directly, and crashes if you add a repetitive object in the release.
Because it will recreate NSDiffableDataSourceSnapshot instances, and if you use the data handling function above in iOS 14 based on NSDiffableDataSourceSnapshot, it won't save the expanded state, and if you change the data, it will collapse all items.
pod 'ConfigableCollectionView'
- High performance filter for appending children.
- Distinguish the achievements of data handling functions above iOS 13 to solve the problem of recreating NSDiffableDataSourceSnapshot instances.
- Remove Proxy.m to support Swift package manager or wait for it support .m files.
- tvOS support.
- Document supplement.