๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
์ด๋ชจ์ €๋ชจ/UIKit

Modern UIKit Collection, TableView #1

by ARpple 2023. 8. 25.

WWDC 19) Advances in UI Data Sources

 

Advances in UI Data Sources - WWDC19 - Videos - Apple Developer

Use UI Data Sources to simplify updating your table view and collection view items using automatic diffing. High fidelity, quality...

developer.apple.com

๐Ÿ’ก
- UI Diffable DataSource๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด automatic diffing(์ž๋™ ๋น„๊ต)๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ CollectionView, TableView ์—…๋ฐ์ดํŠธ(reload)๋ฅผ ๊ฐ„์†Œํ™”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
- ์„ธํŠธ(์…€ ๋ฐ์ดํ„ฐ) ๋ณ€๊ฒฝ์— ๋Œ€ํ•œ ๊ณ ํ’ˆ์งˆ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ์ž๋™์œผ๋กœ ์ œ๊ณต๋˜๋ฉฐ ์ถ”๊ฐ€ ์ฝ”๋“œ๊ฐ€ ํ•„์š”ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค!
- ์ด ๊ฐœ์„ ๋œ ๋ฉ”์ปค๋‹ˆ์ฆ˜์€ ๋™๊ธฐํ™” ๋ฒ„๊ทธ, ์˜ˆ์™ธ ๋ฐ ์ถฉ๋Œ์„ ์™„์ „ํžˆ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค!
- Identifier ๋ฐ SnapShot์„ ์‚ฌ์šฉํ•˜๋Š” ์ด ๊ฐ„์†Œํ™”๋œ Diffable DataSource๋ฅผ ํ†ตํ•ด ๊ฐœ๋ฐœ์ž๋Š” UI ๋ฐ์ดํ„ฐ ๋™๊ธฐํ™”์— ๊ด€ํ•œ ์„ธ๋ฐ€ํ•œ ๋ถ€๋ถ„ ๋Œ€์‹  ์•ฑ์˜ ๋™์  ๋ฐ์ดํ„ฐ์™€ ์ฝ˜ํ…์ธ  ๋ถ€๋ถ„์— ์ง‘์ค‘ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ธฐ์กด ๋ฐฉ์‹์˜ ๋ฌธ์ œ์ 

  1. UI์™€ Controller ์‚ฌ์ด ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ์™€ ๋ทฐ๊ฐ„์˜ ์‹œ๊ฐ„ ์ฐจ์ด ๋ฐœ์ƒ ์‹œ ์ด์— ๋Œ€ํ•œ ์ฒ˜๋ฆฌ์˜ ์–ด๋ ค์›€

๐Ÿ’ก ๋ทฐ ์ดˆ๊ธฐํ™”์‹œ UI(ํ…Œ์ด๋ธ” ๋ทฐ, ์ฝœ๋ ‰์…˜ ๋ทฐ) ์˜์—ญ์ด ์…€ ์•„์ดํ…œ์˜ ๊ฐœ์ˆ˜๋ฅผ ๋ฌป์ง€๋งŒ ์ปจํŠธ๋กค๋Ÿฌ๋Š” ์›น์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„์˜ค๋Š” ์ค‘…
- ๋‚˜์ค‘์— UI์— ๋ฐ์ดํ„ฐ ๊ฐ’์ด ๋ฐ”๋€œ์„ ์•Œ๋ ค์ค˜์•ผํ•จ
  1. ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ ์‚ฌ์ด์— ์…€๊ณผ ์„น์…˜์˜ View์—์„œ์˜ ์‚ญ์ œ, ์ƒ์„ฑ์˜ ๋™์ ์ธ ๋ณ€ํ™” ๋Œ€์‘ ์–ด๋ ค์›€

์‹ค์‹œ๊ฐ„ ์™€์ดํŒŒ์ด ํƒ์ƒ‰ํ•˜๋Š” ํ…Œ์ด๋ธ” ๋ทฐ๋ฅผ ๊ธฐ์กด์˜ ๋ฐฉ์‹์œผ๋กœ ๋ณด์—ฌ์ฃผ๊ธฐ๋Š” ๋„ˆ๋ฌด ๋ณต์žกํ•ด์ง„๋‹ค..!

DiffableDataSource

UI State(ํ…Œ์ด๋ธ” ๋ทฐ, ์ฝœ๋ ‰์…˜ ๋ทฐ ์•„์ดํ…œ ๋ฐ์ดํ„ฐ)์— ๋Œ€ํ•œ ์„ ์–ธํ˜•(Declarative)์ ์ธ ์ ‘๊ทผ

Diffable Data Source ์œ ํ˜•

  • UICollectionViewDiffableDataSource ⇒ ์ฝœ๋ ‰์…˜ ๋ทฐ ๋Œ€์‘
  • UITableViewDiffableDataSource ⇒ ํ…Œ์ด๋ธ” ๋ทฐ ๋Œ€์‘
  • NSCollectionViewDiffableDataSource ⇒ ๋งฅ์— ์กด์žฌํ•˜๋Š” ๋ฆฌ์ŠคํŠธ ํ‘œ์‹œ ์œ ํ˜•

Order of use) ์‚ฌ์šฉ ๊ทœ์น™ ํ˜น์€ ์ˆœ์„œ

  1. diffable ๋ฐ์ดํ„ฐ ์†Œ์Šค๋ฅผ ์ปฌ๋ ‰์…˜ ๋ทฐ์— ์—ฐ๊ฒฐํ•˜์„ธ์š”.
    ⇒ diffable datasource ์ธ์Šคํ„ด์Šค ์ƒ์„ฑํ•˜๊ธฐ
  2. ์…€ ๊ณต๊ธ‰์ž์— ๋Œ€ํ•œ ๊ตฌํ˜„
    ⇒ ์…€ ๋“ฑ๋กํ•˜๊ธฐ
  3. ๋ฐ์ดํ„ฐ์˜ ํ˜„์žฌ ์ƒํƒœ๋ฅผ ์ƒ์„ฑ
    ⇒ ์ด๋ฏธ ์กด์žฌํ•˜๋Š” ๋ฐ์ดํ„ฐ์—์„œ ๊ฐ€์ ธ์˜ด
  4. UI์— ๋ฐ์ดํ„ฐ๋ฅผ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค.
    ⇒ datasource์— ํ˜„์žฌ ๋ฐ์ดํ„ฐ์˜ ์ƒํƒœ๋ฅผ ์ ์šฉ์‹œํ‚ด

Snapshots

  • ํด๋ž˜์Šค: NSDiffableDataSourceSnapshot
  • ํ˜„์žฌ UI State์— ๋Œ€ํ•œ ์ˆœ๊ฐ„์ ์ธ ๊ฐ’
  • ์„น์…˜๊ณผ ๊ทธ ํ•˜์œ„์˜ ์•„์ดํ…œ๋“ค์„ Unique identifiers๋กœ ๊ตฌ๋ถ„ํ•จ
    ⇒ ๊ฐ๊ฐ์˜ ๋ฐ์ดํ„ฐ๋Š” Identifiable ํ”„๋กœํ† ์ฝœ์„ ์ง€์ผœ์•ผํ•œ๋‹ค๋Š” ๋œป
  • ๊ฐ๊ฐ์˜ ์…€ ๋ฐ์ดํ„ฐ๋ฅผ IndexPaths๋กœ ์ง์ ‘ ์ ‘๊ทผํ•ด์„œ ๋ทฐ์— ํ‘œ์‹œํ•  ํ•„์š”๊ฐ€ ์—†์Œ

ํ˜„์žฌ์˜ ๊ฐ’๋“ค์„ ๊ฐ–๊ณ  ์žˆ๋Š” DiffableDataSource์— ์กด์žฌํ•˜๋Š” ์Šค๋ƒ…์ƒท์—์„œ ์ƒˆ๋กœ์šด ์Šค๋ƒ…์ƒท์„ ์ ์šฉ(apply)ํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ์—…๋ฐ์ดํŠธ ํ•œ๋‹ค.

⇒ dataSource์— applyํ•จ์ˆ˜์— snapshot ์ธ์Šคํ„ด์Šค๋ฅผ ๋„ฃ์œผ๋ฉด ๊ทธ ๋ฐ์ดํ„ฐ์— ๋งž๋Š” ์…€ ๋ทฐ๋ฅผ ๋ณด์—ฌ์ค€๋‹ค.
dataSource.apply(snapshot,animating: false)


Required) ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด ์ค€์ˆ˜ํ•ด์•ผํ•˜๋Š” ์‚ฌํ•ญ

  • Section์œผ๋กœ ์‚ฌ์šฉํ•  ํƒ€์ž…๊ณผ Item์œผ๋กœ ์‚ฌ์šฉํ•  ํƒ€์ž…์„ ์„ค์ •ํ•œ๋‹ค.
    • Hashable ์ค€์ˆ˜ ๋ฐฉ๋ฒ• ์ฝ”๋“œ
    • 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 } }
    • โ—์ด๋Ÿฐ ํƒ€์ž…์€ Hashable ํ”„๋กœํ† ์ฝœ์„ ์ค€์ˆ˜ํ•ด์•ผํ•œ๋‹ค.
  • ๊ธฐ์กด DataSource ๋ฐ”์ธ๋”ฉ ๊ธฐ๋ฒ•์€ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค
    • self.tableView.dataSource = self ← ์ด๊ฑฐ ์•ˆ๋จ!!
  • CollectionView, TableView์— ์…€์„ ํ‘œ์‹œํ•  ์…€ ์•„์ดํ…œ์„ ๋ฏธ๋ฆฌ ๋“ฑ๋ก์‹œ์ผœ ๋†“๋Š”๋‹ค.
  • Snapshot apply๋Š” main queue, background queue ๋‘˜ ๋‹ค ์ ์šฉํ•˜์ง€๋งŒ, ํ•˜๋‚˜์˜ ๋ฐ์ดํ„ฐ ์†Œ์Šค์—๋Š” ํ•˜๋‚˜์˜ ํ์—์„œ๋งŒ apply ํ•  ๊ฒƒ

๊ฐ„๋‹จํ•œ ํ…Œ์ด๋ธ” ๋ทฐ์— ์ ์šฉํ•˜๊ธฐ

๐Ÿ’ก ๋ชจ๋“  ํ˜„๋Œ€์ ์ธ ๊ธฐ๋ฒ•์„ ์ ์šฉํ•œ ๊ฒƒ์ด ์•„๋‹Œ ๋ฐ์ดํ„ฐ ์†Œ์Šค์˜ ์ผ๋ถ€ ๊ธฐ๋ฒ•๋งŒ ์ ์šฉํ•จ..!
- ํ…Œ์ด๋ธ” ๋ทฐ ์…€ ์„ ํƒ์‹œ ๊ธฐ์กด์— ์…€์— ์กด์žฌํ•œ ๊ธ€์˜ ์ „์ฒด๋ฅผ ๋ณผ ์ˆ˜ ์žˆ๊ฒŒ ๋งŒ๋“  ์ฝ”๋“œ

ํ…Œ์ด๋ธ” ๋ทฐ ์…€ ์ฝ”๋“œ

import UIKit
import SnapKit
class CustomTableViewCell: UITableViewCell {
    let label = UILabel()
    let button = UIButton()
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        configure()
        setConstraints()
    }
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    func configure(){ // ์…€์— configure์‹œ ์ฃผ์˜์‚ฌํ•ญ
        self.contentView.addSubview(label)
        self.contentView.addSubview(button)
        self.contentView.backgroundColor = .yellow
        label.font = .monospacedDigitSystemFont(ofSize: 17, weight: .medium)
        let config = UIButton.Configuration.filled()
        button.configuration = config
    }
    func setConstraints(){
        button.snp.makeConstraints { make in
            make.width.equalTo(80)
            make.trailingMargin.equalTo(contentView)
            make.top.equalTo(contentView).offset(4)
        }
        label.snp.makeConstraints { make in
            make.verticalEdges.equalTo(contentView).inset(16)
            make.top.leadingMargin.equalTo(contentView)
            make.trailing.equalTo(button.snp.leading).offset(-8)
        }
    }
    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)
    }
}

ํ…Œ์ด๋ธ” ๋ทฐ ์ฝ”๋“œ

import UIKit
import SnapKit

struct Sample: Identifiable,Hashable{ // ํ…Œ์ด๋ธ” ๋ทฐ ์•„์ดํ…œ
    let id = UUID()
    let text: String
    var isExpand: Bool
}

class CustomTableVC: UIViewController{
    let tableView = UITableView()
    enum Section{ case main } // ํ…Œ์ด๋ธ” ๋ทฐ ์„น์…˜... ์‚ฌ์‹ค ํ•„์š” ์—†์ง€๋งŒ ๋ฐ์ดํ„ฐ ์†Œ์Šค์— ํ•„์š”ํ•ด์„œ ๊ฐ„๋‹จํ•˜๊ฒŒ ๋งŒ๋“ฌ
    var dataSource: UITableViewDiffableDataSource<Section,Sample>?
    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(tableView)
        self.view.backgroundColor = .white
        tableView.snp.makeConstraints { $0.edges.equalTo(view.safeAreaLayoutGuide) }
        configureTableView()
    }
}
extension CustomTableVC{
    func configureTableView(){
        tableView.rowHeight = UITableView.automaticDimension
        tableView.separatorStyle = .none
        tableView.delegate = self // ํ…Œ์ด๋ธ” ๋ทฐ์˜ delegate ์ถ”๊ฐ€ํ•˜๊ธฐ
        tableView.register(CustomTableViewCell.self, forCellReuseIdentifier: "customCell") // ํ…Œ์ด๋ธ” ๋ทฐ์— ๋ณด์—ฌ์ค„ ์…€ ๋“ฑ๋กํ•˜๊ธฐ
        /// ํ…Œ์ด๋ธ” ๋ทฐ๊ฐ€ ๋”ฐ๋ผ์•ผํ•  ๋ฐ์ดํ„ฐ ์†Œ์Šค์— ๋Œ€ํ•œ ์„ ์–ธ, ๊ธฐ์กด ๋ฐ์ดํ„ฐ ์†Œ์Šค์˜ cellForRowAt๊ณผ ๋น„์Šทํ•จ
        /// ์ œ๋„ค๋ฆญ์—์„œ ์„น์…˜์— ํƒ€์ž…๊ณผ ์•„์ดํ…œ์˜ ํƒ€์ž…์„ ๋ฏธ๋ฆฌ ์•Œ๋ ค์ค˜์•ผํ•จ,
        /// ํ›„ํ–‰ ํด๋กœ์ ธ์—์„œ ํ…Œ์ด๋ธ” ๋ทฐ, indexPath, ์•ž์—์„œ ์„ ์–ธํ•œ ์•„์ดํ…œ์— ๊ตฌ์กฐ์ฒด์— ๋Œ€ํ•œ ์ธ์Šคํ„ด์Šค๋ฅผ ๋ฐ˜ํ™˜ํ•จ
        /// ํŠน์ • ์ˆœ์„œ์— ์•„์ดํ…œ์— ๋Œ€ํ•œ ์ •๋ณด๋ฅผ ํด๋กœ์ ธ์—์„œ ์•Œ๊ณ  ์žˆ๋‹ค๋Š”๊ฒŒ ๊ธฐ์กด์˜ cellForRowAt๊ณผ ๋‹ค๋ฆ„!!
        self.dataSource = UITableViewDiffableDataSource<Section,Sample>(tableView: tableView) { tableView, indexPath, item in
            guard let cell = tableView.dequeueReusableCell(withIdentifier: "customCell", for: indexPath) as? CustomTableViewCell else {return .init()}
            cell.label.text = item.text
            cell.label.numberOfLines = item.isExpand ? 0 : 2
            cell.button.setTitle("\(indexPath.row) ํ•˜์ด", for: .normal)
            return cell
        }
        // ์ดˆ๊ธฐ์— ์ƒ์„ฑํ•  ์Šค๋ƒ…์ƒท -> ์ด ์Šค๋ƒ…์ƒท์„ ๋ฐ์ดํ„ฐ ์†Œ์Šค์— ์ ์šฉ์‹œํ‚ด
        var snapShot = NSDiffableDataSourceSnapshot<Section, Sample>()
        snapShot.appendSections([.main]) // ์„น์…˜ ์ถ”๊ฐ€ํ•˜๊ธฐ
        // ์ž„์‹œ ์…€ ์•„์ดํ…œ ๋ฐ์ดํ„ฐ๋ฅผ ๋งŒ๋“ค๊ธฐ
        let samples:[Sample] = (0...30).map { val in Sample(text: "์˜ค๋žœ๋งŒ์— ๊ณต์ง€๋กœ ๋Œ์•„์™”์Šต๋‹ˆ๋‹ค.\n\n๊ฐ€์กฑ์—ฌํ–‰ ์ž˜ ๋‹ค๋…€์™”์Šต๋‹ˆ๋‹ค.\n\n์ž˜ ์ง€๋‚ด์…จ๋‚˜์š”? ์‹œ์ฒญ์ž: ๋„ค ์ž˜ ์ง€๋ƒˆ์Šต๋‹ˆ๋‹ค.\n\n๊ทธ๋ ‡๊ตฐ์š”. ์•ž์œผ๋กœ๋„ ์ž˜ ์ง€๋‚ด์‹ญ์‹œ์˜ค.\n\n์žฌ๋ฐŒ๋Š” ์†๋‹˜์„ ๋งŽ์ด ๋ชจ์‹ญ๋‹ˆ๋‹ค. ๊ธฐ๋Œ€ํ•ด ์ฃผ์„ธ์š”. ์›”์š”์ผ๋‚  ๋งŒ๋‚˜์š”~", isExpand: false)}
        snapShot.appendItems(samples)
        self.dataSource?.apply(snapShot)
    }
}
extension CustomTableVC:UITableViewDelegate{
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        /// 1. ๊ธฐ์กด์— ๋ฐ์ดํ„ฐ ์†Œ์Šค์— ์„ ํƒํ•œ indexPath์˜ ๋ฐ์ดํ„ฐ(์•„์ดํ…œ)๋ฅผ ๊ฐ€์ ธ์˜ด
        /// 2. ํ˜„์žฌ ๋ฐ์ดํ„ฐ ์†Œ์Šค์˜ ์Šค๋ƒ…์ƒท์—์„œ ๋ฐ์ดํ„ฐ(์•„์ดํ…œ)๋“ค ๋ฐฐ์—ด์„ ๊ฐ€์ ธ์˜ด (๊ตฌ์กฐ์ฒด ๋ฆฌ์ŠคํŠธ)
        /// 3. ์•„์ดํ…œ ๋ฐฐ์—ด ์ค‘์— ์„ ํƒํ•œ ์•„์ดํ…œ์˜ ์ˆœ์„œ๋ฅผ ์•Œ์•„๋ƒ„
        guard let item = dataSource?.itemIdentifier(for: indexPath),
              var snapshotItems:[Sample] = dataSource?.snapshot().itemIdentifiers,
              let idx = snapshotItems.firstIndex(of: item) else {return}
        snapshotItems[idx].isExpand.toggle() // ์ด ์•„์ดํ…œ์—์„œ ๋ฐ”๋€” ๋ฐ์ดํ„ฐ
        // ์ƒˆ๋กœ์šด ์Šค๋ƒ…์ƒท ์ƒ์„ฑํ•˜๊ธฐ
        var snapsot = NSDiffableDataSourceSnapshot<Section, Sample>()
        snapsot.appendSections([.main])
        snapsot.appendItems(snapshotItems)
        // ๋ฐ์ดํ„ฐ ์†Œ์Šค์— ์ƒˆ๋กœ์šด ์Šค๋ƒ…์ƒท ์ ์šฉ, ์ด์ „ ์Šค๋ƒ…์ƒท๊ณผ ๋‹ค๋ฅธ ์ ์ด ์žˆ๋‹ค๋ฉด ์• ๋‹ˆ๋ฉ”์ด์…˜ ์‹คํ–‰
        dataSource?.apply(snapsot,animatingDifferences: true)
    }
}

๋Œ“๊ธ€