Last active
October 29, 2020 10:13
-
-
Save vinhnx/22c16be8d6a74efb625231540b26ee36 to your computer and use it in GitHub Desktop.
Diffable datasource and compositional layout
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Diffable datasource and compositional layout: | |
+ https://www.donnywals.com/modern-table-views-with-diffable-data-sources/ | |
+ https://www.swiftbysundell.com/articles/building-modern-collection-views-in-swift/ | |
> **UICollectionViewDiffableDataSource** let us compute UICollectionView datasource and configuration, as a replacement for plain old UICollectionViewDatasource/UICollectionViewDelegate. We also have table view counter part, they are **UITableViewDiffableDataSource**. | |
> we can now use **UICollectionViewComposableLayout** as replacement for **UITableView**. | |
```swift | |
func makeCollectionView() -> UICollectionView { | |
let layout = makeCollectionViewCompositionalLayout() | |
return UICollectionView(frame: .zero, collectionViewLayout: layout) | |
} | |
func makeGridLayoutSection() -> NSCollectionLayoutSection { | |
// Each item will take up half of the width of the group | |
// that contains it, as well as the entire available height: | |
let item = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize( | |
widthDimension: .fractionalWidth(0.5), | |
heightDimension: .fractionalHeight(1) | |
)) | |
// Each group will then take up the entire available | |
// width, and set its height to half of that width, to | |
// make each item square-shaped: | |
let group = NSCollectionLayoutGroup.horizontal( | |
layoutSize: NSCollectionLayoutSize( | |
widthDimension: .fractionalWidth(1), | |
heightDimension: .fractionalWidth(0.5) | |
), | |
subitem: item, | |
count: 2 | |
) | |
return NSCollectionLayoutSection(group: group) | |
} | |
func makeListLayoutSection() -> NSCollectionLayoutSection { | |
let item = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(50))) | |
let inset: CGFloat = 10 | |
item.contentInsets = NSDirectionalEdgeInsets(top: inset, leading: 0, bottom: 0, trailing: 0) | |
let size = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1)) | |
let group = NSCollectionLayoutGroup.vertical(layoutSize: size, subitems: [item]) | |
let section = NSCollectionLayoutSection(group: group) | |
section.contentInsets = NSDirectionalEdgeInsets(top: inset, leading: 0, bottom: 0, trailing: 0) | |
section.interGroupSpacing = 10 | |
// Supplementary header view setup | |
let headerFooterSize = NSCollectionLayoutSize( | |
widthDimension: .fractionalWidth(1.0), | |
heightDimension: .estimated(20) | |
) | |
let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem( | |
layoutSize: headerFooterSize, | |
elementKind: UICollectionView.elementKindSectionHeader, | |
alignment: .top | |
) | |
section.boundarySupplementaryItems = [sectionHeader] | |
return section | |
} | |
func makeCollectionViewCompositionalLayout() -> UICollectionViewCompositionalLayout { | |
return UICollectionViewCompositionalLayout { (sectionIndex, _) in | |
// we can even switch section index to choose custom layout section | |
switch sectionIndex { | |
case 0: | |
return makeGridLayoutSection() | |
default: | |
return makeListLayoutSection() | |
} | |
} | |
} | |
``` | |
usage example: | |
```swift | |
internal typealias DataSource = UICollectionViewDiffableDataSource<Section, Event> | |
final class EventListViewController: BaseViewController { | |
// MARK: - Properties | |
private var collectionView: UICollectionView = makeCollectionView() | |
private lazy var datasource = makeDatasource() | |
// MARK: - Override | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
setupLayout() | |
fetchEvents() | |
} | |
override func setupViews() { | |
super.setupViews() | |
view.backgroundColor = .backgroundColor | |
} | |
// MARK: - Public | |
func fetchEvents(for date: Date = Date()) { | |
EventHandler.shared.fetchEvents(for: date) { [weak self] result in | |
guard let self = self else { return } | |
switch result { | |
case .success(let value): | |
let items = value.map { Event(event: $0) } | |
self.applySnapshot(items, date: date) | |
case .failure(let error): | |
logError(error) | |
} | |
} | |
} | |
// MARK: - Datasource | |
func makeDatasource() -> DataSource { | |
let datasource = DataSource( | |
collectionView: collectionView, | |
cellProvider: { (collectionView, indexPath, event) in | |
guard let event = event.event else { return nil } | |
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: EventListTableViewCell.reuseID, for: indexPath) as? EventListTableViewCell | |
let viewModel = EventListTableViewCell.ViewModel(event: event) | |
cell?.viewModel = viewModel | |
return cell | |
} | |
) | |
datasource.supplementaryViewProvider = { collectionView, kind, indexPath in | |
guard kind == UICollectionView.elementKindSectionHeader else { return nil } | |
let section = self.datasource.snapshot().sectionIdentifiers[indexPath.section] | |
let view = collectionView.dequeueReusableSupplementaryView( | |
ofKind: kind, | |
withReuseIdentifier: SectionHeaderReusableView.reuseID, | |
for: indexPath) as? SectionHeaderReusableView | |
view?.titleLabel.text = section.date?.toFullDateString | |
return view | |
} | |
return datasource | |
} | |
func applySnapshot(_ items: [Event], date: Date?) { | |
guard let date = date else { return } | |
var snapshot = NSDiffableDataSourceSnapshot<Section, Event>() | |
let section = Section(date: date, events: items) | |
snapshot.appendSections([section]) | |
snapshot.appendItems(items, toSection: section) | |
datasource.apply(snapshot, animatingDifferences: true, completion: nil) | |
} | |
private func setupLayout() { | |
collectionView = makeCollectionView() | |
collectionView.backgroundColor = .backgroundColor | |
// section header | |
collectionView.register( | |
SectionHeaderReusableView.self, | |
forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, | |
withReuseIdentifier: SectionHeaderReusableView.reuseID | |
) | |
// cell | |
collectionView.register( | |
UINib(nibName: EventListTableViewCell.reuseID, bundle: nil), | |
forCellWithReuseIdentifier: EventListTableViewCell.reuseID | |
) | |
collectionView.dataSource = datasource | |
collectionView.delegate = self | |
view.addSubViewAndFit(collectionView) | |
} | |
} | |
``` | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment