TableView ๋ฆฌํฉํ ๋ง ํ๋ค๊ฐ ๋ ๊ฑฐ์๋ฅผ ๋ค๋ฅด๊ฒ ๋ฐ๊ฟ ์ ์์๊นํ๋ฉด์ ์์๋ณด์๋ค.
์ด๊ฑธ ๋ฐฐ์ฐ๋ฉด ์ด๋ฐ๊ฒ ๊ฐ๋ฅํ๋ค. ์๋์ผ๋ก ์ด๋ ๊ฒ ์ ๋๋ฉ์ด์ ์ ์ฒ๋ฆฌํด์ค๋ค! iOS 13์์๋ถํฐ ์ ์ฉ๊ฐ๋ฅํ๋ค.
Current State-of-the-Art
// MARK: UICollectionViewDataSource
func numberOfSections(in collectionView: UICollectionView) -> Int {
return models.count
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return models[section].count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for:indexPath)
// configure cell
return cell
}
์ด๊ฒ ์์ ์ ํ๋ ๋ฐฉ์์ด๋ค. section ๊ฐ์ ์ ํด์ฃผ๊ณ , section์์ ๊ฐ์๋ฅผ ์ ํด์ฃผ๊ณ , delegate๋ก ํจ์๊ฐ ํธ์ถ์ด ๋์์ ๋, ์ด๋ป๊ฒ cell์ ๋ง๋ค์ด์ค ๊ฒ์ธ์ง ์ ์ํด์ฃผ๋ฉด ๋๋ ๋ฐฉ์์ด๋ค.
UICollectionViewDataSource
@MainActor protocol UICollectionViewDataSource
์ด๋ ๊ฒ ์ฌ์ฉํ ์ ์์๋ ํ ๋๋ UICollectionViewDataSource
๋ฅผ ์ฌ์ฉํ๊ณ ์๊ธฐ ๋๋ฌธ์ด๋ค. ์ด๋
์์ protocol์ด๊ณ , viewController์์ self.dataSource = self
๋ฅผ ํตํด ์์ ๊ฐ์ด delegate๋ก ์ฒ๋ฆฌ๊ฐ ๊ฐ๋ฅํ ์ด์ ๋ viewController๊ฐ UICollectionViewDataSource
๋ฅผ ์ฑํํ๊ณ ์๊ธฐ ๋๋ฌธ์ด๋ค.
Apps Are Often Complicated
ํ์ง๋ง ์ค์ ๋ก 1์ฐจ์ ๋ฐฐ์ด์์๋ ํธ๋ฆฌํ์ง๋ง, ๋ณต์กํ ๊ฒฝ์ฐ ์ด๋ฌํ ์ ๊ทผ ๋ฐฉ์์ ์๋นํ ๋จธ๋ฆฌ์ํ๋ค.
- Web service
- Core Data
- ๋ค์ ViewController๋ก ๋ถํฐ ์ ๋ฐ์ดํธ ๋๋ ๊ฒฝ์ฐ
์ ๋ง ๋จ์ํ์ง๋ง, ๊ฐ๋ ์ฐ๋ฆฌ๋ ์ด๋ฐ ์๋ฌ๋ฅผ ๋ง์ฃผํ๊ฒ ๋๋๋ฐ..
StackOverflow์์ ์ฐพ์๋ณด๊ณ ๊ฒฐ๊ตญ, ์ฐ๋ฆฌ๋ reloadData
๋ฅผ ์ ํํ๋ค. WWDC์์๋ ์ ํ ๋ฌด๋ฐฉํ ํ๋์ด๋ผ๊ณ ํ๊ธดํ์ง๋ง, ์ด๋ ๊ฒ ํ ๊ฒฝ์ฐ ์ ๋๋ฉ์ด์
๋์ง ์์ ํจ๊ณผ๊ฐ ๋ํ๋๋ค.
What is the problem
์ด๋ฌํ ์ํฉ์์ ๋ฌธ์ ๋ ์ด๋์ โ์ง์งโ๊ฐ ์๋๋์ด๋ค. ์ฆ, DataSource ์ญํ ์ ํ๋ DataController๊ฐ ์๊ฐ์ด ์ง๋จ์ ๋ฐ๋ผ ๋ณํํ๋ ์์ ์ version, Truth๋ฅผ ๊ฐ์ง๊ณ ์๋ค๋ ๊ฒ์ด๋ค. (own version of the truth) ๊ทธ๋ฆฌ๊ณ UI ์ญ์ ํ๋ฉด์ ๋ณด์ฌ์ฃผ๊ณ ์๋ truth๋ฅผ ๊ฐ์ง๊ณ ์๋ค. ์ด ๋๊ฐ๊ฐ ์๋ก ๋ง์ง ์์ ๊ฒฝ์ฐ ์์ ์๋ฌ๊ฐ ๋ฟ!ํ๊ณ ๋์จ๋ค.
๊ฒฐ๊ตญ, ์ด ๋ฌธ์ ๋ ์ค์์ง์คํ์ผ๋ก ํต์ ๋๊ณ ์๋ truth๊ฐ ์๋ค๋ ๊ฒ์ด ์์ธ์ด๋ค.
New Approach
๊ทธ๋์ Apple์ ์์ ํ ์๋ก์ด ์ ๊ทผ๋ฐฉ์์ ๋์ ํ๋ค. ๊ทธ๊ฒ์ด Diffable DataSource์ด๋ค!
Diffable Data Source
performBatchUpdates()
๊ฐ ์์ด์ง๋ค. (์ฐ์ฐ์ ํตํด animation ์ฃผ๋ method) ๊ท์ฐฎ๊ณ , ๋ณต์กํ๊ณ crash ์ค๋ค. ์ด๋ฐ ๊ฒ ํ์์๊ณ ์ด์ apply
ํ๋ฐฉ์ด๋ฉด ์๋์ผ๋ก ๋ณํ๋ฅผ ๊ฐ์งํด์ ์ ์ฉํ๋ค.
Snapshots
์ด๊ฑธ ๊ฐ๋ฅํ๊ฒ ํ๊ธฐ ์ํด์ ์๋ก์ด structure๋ฅผ ์ถ๊ฐํ๋๋ฐ, ๊ทธ๋ ์์ snapshot์ด๋ค. ํ์ฌ UI State์ truth๋ฅผ ๊ฐ์ง ๋ ์์ด๋ค.
- Truth of UI State
- Unique identifiers for sections and items
- No more IndexPaths
์ด์ ๋ถํฐ IndexPath๊ฐ ์๋๊ณ identifier๋ก ์ด๋ฅผ ๊ตฌ๋ถํ๋ค.
SnapShot์ด ์ ์ฉ๋๋ ์ง๊ด์ ์ธ ๊ทธ๋ฆผ์ ์์ ๊ฐ๋ค. ์๋ก์ด Snapshot์ผ๋ก ๊ธฐ์กด๊ฒ์ ๋์ฑํ๋ ๊ฒ! ์ด ๋, Animation์ system์ด ์๋์ผ๋ก ์ ์ฉํ๋ค.
DiffableDataSource
- UICollectionViewDiffableDataSource
- UITableViewDiffableDataSource
- NSCollectionViewDiffableDataSource (MacOS)
- NSDiffableDataSourceSnapshot (Common)
์ฌ๊ธฐ์ ์ฃผ๋ชฉํ ์ ์, ์ด๋ ์๋ค์ ๋์ด์ Protocol์ด ์๋๋ผ๋ ์ ์ด๋ค. class์ด๋ฉฐ, ์ฌ์ฉํ ์ ๋ช ์์ ์ผ๋ก ์ ์ธํ๊ณ apply ํด์ฃผ์ด์ผ ํ๋ค.
Demo!
์์ผ๋ก ์ค๋ช ํ ์ฝ๋๋ Implementing Modern Collection Views์ ์๋ค.
๊ฒ์์ฐฝ์ ์ ๋ ฅํ๋ฉด, ์ด์ ๋ง๋ ์ฐ์ ํํฐ๋งํด์ฃผ๊ณ ๋ณด์ฌ์ฃผ๋ ๋์์ ํ๋ ์ฑ์ด๋ค. ํ๋ฆ์ ๋ค์๊ณผ ๊ฐ๋ค.
- Search bar์ text๊ฐ ๋ณ๊ฒฝ๋์์ ์ callback ํจ์๊ฐ ๋ถ๋ฆฐ๋ค.
- callback ํจ์๋ด์์๋ ํด๋น ์ ๋ ฅ์ผ๋ก ๊ฒฐ๊ณผ๋ฅผ ๋ฐ์์ค๋ queryํจ์๋ฅผ ํธ์ถํ๋ค.
- ํธ์ถํ๋ query ํจ์๋ด์์๋ model layer์์ ๊ฐ์ ๋ฐ์์ค๊ณ , ์๋ก์ด snapshot์ ์ฐ๋๋ค.(์ธ์คํด์ค ์์ฑ)
- ์ฐ์ snapshot์ ํ์ฌ diffableDataSource์ applyํ๋ค.
Instructions
- Connect a diffable data source to your collection view.
- Implement a cell provider to configure your collection viewโs cells.
- Generate the current state of the data.
- Display the data in the UI.
Connect a diffable data source to your collection view.
class MountainsViewController: UIViewController {
var dataSource: UICollectionViewDiffableDataSource<Section, MountainsController.Mountain>!
}
๋จผ์ , CollectionView์ dataSource๋ฅผ ๋ง๋ค์ด์ค๋ค.
@MainActor class UICollectionViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType> : NSObject where SectionIdentifierType : Hashable, ItemIdentifierType : Hashable
์ด ๋, ๋๊ฐ์ Type์ ๋ฐ๋๋ฐ ๋ชจ๋ Hashable์ด์ด์ผ ํ๋ค.
SectionIdentifier Type
class MountainsViewController: UIViewController {
enum Section: CaseIterable {
case main
}
var dataSource: UICollectionViewDiffableDataSource<Section, MountainsController.Mountain>!
}
Apple ์์๋ Section์ ๊ธฐ๋ณธ์ ์ผ๋ก Enum์ผ๋ก ์ฌ์ฉํ๊ณ ์์๋ค. Enum์ ๊ฒฝ์ฐ์๋ ์ฐ๊ด๊ฐ์ด ์๊ฑฐ๋, ์ฐ๊ด๊ฐ์ด ๋ชจ๋ Hashableํ ๊ฒฝ์ฐ ์๋์ผ๋ก synthesize๋๋ค.
For an enum, all its associated values must conform to Hashable. (An enum without associated values has Hashable conformance even without the declaration.)
ItemIdentifier Type
ItemIdentifier ์ญ์ ๊ณ ์ ํด์ผ ํ๋ค. ์ด๋ฌํ ๊ฒฝ์ฐ Apple์์๋ UUID๋ฅผ ์ฌ์ฉํ์ฌ ์ด๋ฅผ ๊ตฌํํ๋ค.
class MountainsViewController: UIViewController {
enum Section: CaseIterable {
case main
}
var dataSource: UICollectionViewDiffableDataSource<Section, MountainsController.Mountain>!
}
class MountainsController {
struct Mountain: Hashable {
let name: String
let height: Int
let identifier = UUID()
func hash(into hasher: inout Hasher) {
hasher.combine(identifier)
}
static func == (lhs: Mountain, rhs: Mountain) -> Bool {
return lhs.identifier == rhs.identifier
}
}
}
Mountain์ ๋ณด๋ฉด, Hashable์ ์ฑํํ๊ณ ์์ผ๋ฉฐ, ๊ฐ instance๊ฐ ๋ ๋ฆฝ์ ์ด๊ธฐ ์ํด UUID๋ฅผ ํตํด Equality๋ฅผ ์ฒ๋ฆฌํด์ฃผ๊ณ ์๋ค.
Connect CollceionView, Provider
์ด์ dataSource๋ฅผ ์ค์ ์ธ์คํด์ค๋ก ๋ง๋ค์ด์ ๋ฃ์ด์ฃผ๋ฉด ๋๋ค. ์ธ์๋ก collectionView
์ cellProvider
๋ฅผ ๋ฐ๋๋ค.
@MainActor init(collectionView: UICollectionView, cellProvider: @escaping UICollectionViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType>.CellProvider)
collectionView
์ ๊ฒฝ์ฐ ํด๋น VC์์ ์ ์ธํ collcetionView๋ฅผ ๋ฃ์ด์ฃผ๋ฉด ๋๊ณ , cell provider์ ๊ฒฝ์ฐ 3๊ฐ์ ์ธ์(sectionIdentifier, indexPath, itemIdentifier)๋ฅผ ๊ฐ๋ closure์ด๋ค.
dataSource = UICollectionViewDiffableDataSource<Section, MountainsController.Mountain>(collectionView: mountainsCollectionView) {
(collectionView: UICollectionView, indexPath: IndexPath, identifier: MountainsController.Mountain) -> UICollectionViewCell? in
// Return the cell.
}
์ฌ๊ธฐ์ closure ์ธ์๋ ์ด์ ์ DiffableDataSource
๋ฅผ ์ ์ธํ์ ๋ sectionIdentifier์ itemIdentifier๊ฐ ๋ค์ด์ค๊ฒ ๋๋ค.
Implement a cell provider to configure your collection viewโs cells.
dataSource = UICollectionViewDiffableDataSource<Section, MountainsController.Mountain>(collectionView: mountainsCollectionView) {
(collectionView: UICollectionView, indexPath: IndexPath, identifier: MountainsController.Mountain) -> UICollectionViewCell? in
// Return the cell.
guard let cell = collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: identifier) else { return }
cell.configure(mountain: identifier)
return cell
}
Apple ์ฝ๋์์๋ dequeueConfiguredReusableCell
๋ฅผ ํตํด ์ฌํ์ฉ๋ cell์ ์ ๊ณตํ๋ ๊ฒ์ผ๋ก ๋ง์ณค๋ค. ์์ฑํด์ ์ ๊ณตํ ์๋ ์์ ๊ฒ์ด๋ค.
Generate the current state of the data.
์ด์ ์ฐ๋ฆฌ๊ฐ ์ค์ ๋ง๋ค ์ฑ์์ interaction๊ณผ ์ฐ๊ฒฐํด์ค ์ฐจ๋ก์ด๋ค. ์ฐ๋ฆฌ๋ ์ฌ์ฉ์ text ์ ๋ ฅ์ ๋ฐ๋ผ ์ ์ฉํ ๊ฒ์ด๋ค.
// 01: Event ๋ฐ์, 02: query ํจ์ ํธ์ถ
extension MountainsViewController: UISearchBarDelegate {
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
performQuery(with: searchText)
}
}
// 03: ๋ชจ๋ธ๋ก๋ถํฐ ๋ณ๊ฒฝ๋ ๊ฐ์ ๊ฐ์ ธ์ด
extension MountainsViewController {
func performQuery(with filter: String?) {
let mountains = mountainsController.filteredMountains(with: filter).sorted { $0.name < $1.name } // model์์ ๊ฒฐ๊ณผ๋ฅผ ๊ฐ์ ธ์ด
var snapshot = NSDiffableDataSourceSnapshot<Section, MountainsController.Mountain>() // snapshot ์์ฑ
snapshot.appendSections([.main])
snapshot.appendItems(mountains)
dataSource.apply(snapshot, animatingDifferences: true) // 04: apply
}
}
snapshot์ ์ฐ์ ๋น์์๋ ๋น์ด์๊ธฐ ๋๋ฌธ์, ์ฌ๊ธฐ์ ์ด๋ป๊ฒ ๋ณด์ฌ์ง ์ง ์ค์ ํด์ผ ํ๋ค. section์ ๊ฒฝ์ฐ ๋จ์ผ์ด๊ธฐ ๋๋ฌธ์ [.main]
๋ง ๋ฃ์ด์ฃผ๊ณ , item์ ๊ฒฝ์ฐ ์๋๋ identifier๊ฐ ๋ค์ด๊ฐ์ผ ํ์ง๋ง, Swift์ ๊ฒฝ์ฐ ๋ณด๋ค elegantํ๊ฒ ์๋ํ๊ธฐ ์ํด own native type์ ๋ฃ์ด๋ ๋์ํ๋ค.
Display the data in the UI.
![]() | ![]() |
animation์ true๋ก ์ฃผ๋ฉด, ์์๊ฐ์ด ์์ ์ ๋๋ฉ์ด์ ์ด ์ฆ๊ฐ ์ ์ฉ๋๋ค. false์ธ ๊ฒฝ์ฐ ์ค๋ฅธ์ชฝ๊ณผ ๊ฐ์ด ๋์จ๋ค.
์ถ๊ฐ
storyboard๋ xib๋ฅผ ์ฌ์ฉํ์ฌ ๋ง๋ค์ด์ง ๋ ์์ ๊ฒฝ์ฐ์๋ register๊ฐ ํ์๋ค.
self.collectionView.register(DJCollectionViewCell.self, forCellWithReuseIdentifier: "cell")
๊ทธ๋ฆฌ๊ณ ๋์ dequeue๋ฅผ ๋ค์๊ณผ ๊ฐ์ ๋ฐฉ์์ผ๋ก ํด์คฌ์๋ค.
dataSource = UICollectionViewDiffableDataSource<Section, MountainsController.Mountain>(collectionView: mountainsCollectionView) {
(collectionView: UICollectionView, indexPath: IndexPath, identifier: MountainsController.Mountain) -> UICollectionViewCell? in
// Return the cell.
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as? MountainCollectionViewCell else { preconditionFailure() }
cell.configure(mountain: identifier)
return cell
}
๊ทธ๋ฐ๋ฐ, ์ด์์
์์ด diffabledataSource
๋ฅผ ๋ง๋ค ๋, dequeueConfiguredReusableCell
์ ์ฌ์ฉํ๋ฉด register์ ๋์์ configuration๊น์ง ์ฒ๋ฆฌํ ์ ์๋ค. ์ด ๋ฐฉ์์ด Apple์์ ์ฌ์ฉํ ๋ฐฉ์์ด๋ค.
dataSource = UICollectionViewDiffableDataSource<Section, MountainsController.Mountain>(collectionView: mountainsCollectionView) {
(collectionView: UICollectionView, indexPath: IndexPath, identifier: MountainsController.Mountain) -> UICollectionViewCell? in
// Return the cell.
guard let cell = collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: identifier) else { return }
cell.configure(mountain: identifier)
return cell
}
Considerations
Current Snapshot
// Empty snapshot
let snapshot = NSDiffableDataSourceSnapshot<Section, UUID>()
// Current data source snapshot copy
let snapshot = dataSource.snapshot()
ํ์ฌ snapshot์ ๊ฐ์ ธ์ฌ ์ ์๊ณ , ๋ง๋ค ์ ์๋ค. ์๋ ํจ์๋ฅผ ํธ์ถํ๋ฉด copyํด์ ๊ฐ๋ค์ค๋ค. ๊ทธ๋์ ์ด์ snapshot์ ์ํฅ์ ๋ฏธ์น์ง ์๋๋ค.
Snapshot State
// Snapshot State
var numberOfItems: Int { get }
var numberOfSections: Int { get }
var sectionIdentifiers: [SectionIdentifierType] { get }
var itemIdentifiers: [ItemIdentifierType] { get }
snapshot์ ๋ค์ํ ์ํ๋ฅผ ํ์ธํ ์ ์๋ค.
Configuring Snapshots
// Configuring Snapshots
func insertItems(_ identifiers: [ItemIdentifierType],
beforeItem beforeIdentifier: ItemIdentifierType)
func moveItem(_ identifier: ItemIdentifierType,
afterItem toIdentifier: ItemIdentifierType)
func appendItems(_ identifiers: [ItemIdentifierType],
toSection sectionIdentifier: SectionIdentifierType? = nil)
func appendSections(_ identifiers: [SectionIdentifierType])
snapshot์ ๊ตฌ์ถํ๋๋ฐ ์์ด์๋ ๋ค์ํ method๋ฅผ ์ ๊ณตํ๋ค.
Custom Identifiers
// Custom Identifiers
struct MyModel: Hashable {
let identifier = UUID()
func hash(into hasher: inout Hasher) {
hasher.combine(identifier)
}
static func == (lhs: MyModel, rhs: MyModel) -> Bool {
return lhs.identifier == rhs.identifier
}
}
Identifier๋ uniqueํ๊ณ , hashable์ ์ฑํํด์ผ ํ๋ค.
Get Identifier busing indexPath
// What About IndexPath-based APIs?
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if let identifier = dataSource.itemIdentifier(for: indexPath) {
// Do something
}
}
indexPath๋ก๋ถํฐ identifier๋ฅผ ๊ฐ์ ธ์ค๋ API๋ ์ ๊ณตํ๋ค. ํด๋น API์ ์๊ฐ๋ณต์ก๋๋ Constant๋ผ๊ณ ํ๋ค.
Performance
๊ต์ฅํ ๋น ๋ฅด๋ค๊ณ ์๋ํ๋ค. O(n)์ด๋ผ๊ณ ํ๋ค. ๊ทธ๋ฆฌ๊ณ apply()
ํธ์ถ์ background queue์์ Safeํ๋ค๊ณ ํ๋ค!! ์์์ ํด์ค๋ค!!
Summary
- Model์ ๊ด๋ฆฌํ๋ controller์ ์ค์ UI์์ ๋ณด์ด๋ Truth์ ์ฐจ์ด๋ก DataSource๋ฅผ ๊ด๋ฆฌํ๋ ๊ฒ์ด ์ด๋ ต๋ค.
- ์ด๋ฌํ ์ ์์ Apple์ Diffable Datasource๋ฅผ ๋ง๋ค์๋ค.
- Protocol ๋ฐฉ์์ด ์๋ Class ๋ฐฉ์์ ์ฌ์ฉํ์ฌ Snapshot์ ์ฐ๊ณ ์ด๋ฅผ apply ํจ์ผ๋ก์จ ๋ณํ๋ฅผ ํตํ ์ ๋๋ฉ์ด์ ์ ์๋์ผ๋ก ์ฒ๋ฆฌํด์ค๋ค.
- Diffable DataSource ์์ฑ, Cell Provider ์ ์, Snapshot ์์ฑ, Diffable DataSource์ ์ ์ฉ์ 4๋จ๊ณ๋ก ์ฒ๋ฆฌ๊ฐ ๊ฐ๋ฅํ๋ค.
- SectionIdentifier์ ItemIdentifier๋ Hashable์ด์ด์ผ ํ๋ฉฐ, ์ด ๋ UUID๋ฅผ ํ์ฉํ ์ ์๋ค.
- ์๋๊ฐ ๊ต์ฅํ ๋น ๋ฅด๋ค! ๊ทธ๋ฆฌ๊ณ background queue์์
apply()
๋ฅผ ํธ์ถํด๋ Safeํ๋ค. - iOS 13์์๋ถํฐ ์ ์ฉ๊ฐ๋ฅํ๋ค.
๋ง์ ๋ณด๋ค๋ณด๋ Hashable๊ฐ์ ํ๋กํ ์ฝ์ ๋ํด ์ ํํ ๋ชจ๋ฅด๋ ๊ฒ ๊ฐ๋ค. ๋ค์ ํฌ์คํ ์์๋ ์ด๋ ์์ ๋ค๋ค๋ณด๋๋ก ํ๊ฒ ๋ค. ๋!