๐Ÿ‘จโ€๐ŸŽ“
Today I Learned
  • Today-I-Learend
  • ๐ŸŽWWDC
    • Developer Tools
      • Testing in Xcode
    • UIKit
      • UIDiffableDataSource
        • [WWDC 19] Advances in UI Data Sources
      • [WWDC2019] Advances in CollectionView Layout
  • ์ž๋ฃŒ๊ตฌ์กฐ
    • Heap ์ž๋ฃŒ๊ตฌ์กฐ
  • Clean code
    • ๋„ค์ด๋ฐ
    • ์ฃผ์„๊ณผ ํฌ๋งทํŒ…
    • ํ•จ์ˆ˜
    • ํด๋ž˜์Šค
    • ์—๋Ÿฌ ํ•ธ๋“ค๋ง
    • ๊ฐ€๋…์„ฑ ๋†’์ด๊ธฐ
    • ๊ฐ์ฒด์ง€ํ–ฅ
  • Network
    • RestAPI
  • Swift
    • DateType
    • ARC
    • Availablity
    • KeyPath
    • Network
    • Neverํƒ€์ž…
    • Result
    • Selector
    • ๊ฒ€์ฆํ•จ์ˆ˜
    • ๋ฉ”ํƒ€ํƒ€์ž…
    • ๋™์‹œ์„ฑ ํ”„๋กœ๊ทธ๋ž˜๋ฐ
    • ๋ฉ”๋ชจ๋ฆฌ ์•ˆ์ „
    • ์—๋Ÿฌ์ฒ˜๋ฆฌ
    • ์ ‘๊ทผ์ œ์–ด (Access Control)
    • ์ œ๋„ค๋ฆญ
    • ์ฃผ์š” ํ”„๋กœํ† ์ฝœ
  • ์•Œ๊ณ ๋ฆฌ์ฆ˜
    • ๊ทธ๋ž˜ํ”„
    • ๊ธฐ์ดˆ ์•Œ๊ณ ๋ฆฌ์ฆ˜
    • ๋ˆ„์ ํ•ฉ(Prefix)
    • ๋ณต์žก๋„
    • ๋น„ํŠธ๋งˆ์Šคํ‚น
  • ์šด์˜์ฒด์ œ
    • ์šด์˜์ฒด์ œ์˜ ๊ฐœ์š”
    • ํ”„๋กœ์„ธ์Šค์™€ ์Šค๋ ˆ๋“œ
    • CPU ์Šค์ผ€์ค„๋ง
    • ํ”„๋กœ์„ธ์Šค ๋™๊ธฐํ™”
    • ๊ต์ฐฉ์ƒํƒœ
    • 07. ๋ฉ”๋ชจ๋ฆฌ
    • 08.๊ฐ€์ƒ ๋ฉ”๋ชจ๋ฆฌ
    • ์ž…์ถœ๋ ฅ ์žฅ์น˜
    • ํŒŒ์ผ ์‹œ์Šคํ…œ
  • UIKit
    • UITableView xib์œผ๋กœ ๋งŒ๋“ค์–ด๋ณด๊ธฐ
  • ๐Ÿ–Š๏ธ์ •๋ณด ๊ธฐ๋ก
    • ์ฝ”์ฝ”์•„ํŒŸ ๋ฐฐํฌํ•˜๋Š” ๋ฐฉ๋ฒ•
  • iOS Project
    • ์ฑ„ํŒ… ์•ฑ ๋งŒ๋“ค๊ธฐ
      • Trouble shooting
      • 1. ๋””์ž์ธ
      • 2. AutoLayout
    • ๋‚ ์”จ ์กฐํšŒ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜
      • Figma๋ฅผ ์ด์šฉํ•œ UI ์„ค๊ณ„
      • TableView ์—ฐ๊ฒฐํ•˜๊ธฐ
      • Networking
    • MVC -> MVVM์œผ๋กœ ๊ตฌ์กฐ ๋ณ€๊ฒฝํ•ด๋ณด๊ธฐ
      • MVC
      • MVVM
    • OAuth Project
      • ๋กœ์ปฌ ํ˜ธ์ŠคํŠธ๋ฅผ ์ด์šฉํ•œ ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€ ์ œ์ž‘
      • Github์˜ OAuth App ์„ค์ •
    • Rest API ํ”„๋กœ์ ํŠธ
      • UI์„ค๊ณ„ (with Figma)
      • Network Model
      • MVVM ๊ตฌ์กฐ ์ „ํ™˜
  • ๐Ÿ•ถ๏ธUIKit
    • Compositional Layout
Powered by GitBook
On this page
  1. WWDC
  2. UIKit
  3. UIDiffableDataSource

[WWDC 19] Advances in UI Data Sources

KEY POINT! (์š”์•ฝ)

  • Diffabla DataSource์˜ ๋“ฑ์žฅ

    • UICollectionView, UITableView์— ์ ์šฉ์ด ๊ฐ€๋Šฅํ•˜๋‹ค. (iOS13)

    • snapshot์ด๋ผ๋Š” ์ƒˆ๋กœ์šด ๊ฐœ๋…์ด ๋“ฑ์žฅํ•˜๋ฉด์„œ, performBatchUpdates๋ฅผ ์‚ฌ์šฉํ•  ํ•„์š”๊ฐ€ ์—†์–ด์กŒ๋‹ค.

  • Diffable DataSource์˜ ์‚ฌ์šฉ ๋ฐฉ๋ฒ•

    1. NSDiffableDataSource๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.

    2. Hashable Protocol์„ ์ฑ„ํƒํ•œ ์‹๋ณ„์ž๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค.

    3. snapshot์„ ์ ์šฉํ•œ๋‹ค.

      • ๋ณ€๊ฒฝ๋œ ๋ถ€๋ถ„์„ Diffable DataSource๊ฐ€ ์ž๋™์œผ๋กœ ์ฒ˜๋ฆฌํ•ด์„œ ์ž์—ฐ์Šค๋Ÿฌ์šด ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ ์šฉํ•œ๋‹ค.


WWDC ์˜์ƒ ๋ชฉ์ฐจ

  • ํ˜„์žฌ CollectionView์™€ TableView์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์–ด๋–ป๊ฒŒ ๊ด€๋ฆฌํ•˜๊ณ  ์žˆ๋Š”๊ฐ€?

  • ์ƒˆ๋กœ์šด Data ๊ด€๋ฆฌ ๋ฐฉ๋ฒ•

  • ์•ฑ์„ ํ†ตํ•œ ๋ฐ๋ชจ

  • Diffable DataSource๋ฅผ ํ™œ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•


ํ˜„์žฌ CollectionView์™€ TableView์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์–ด๋–ป๊ฒŒ ๊ด€๋ฆฌํ•˜๊ณ  ์žˆ๋Š”๊ฐ€?

๊ฐœ๋ฐœํ•  ๋•Œ TableView, CollectionView๋Š” ์ •๋ง ๋นผ๋†“์„ ์ˆ˜ ์—†๋‹ค. ์–ด๋–ค ์•ฑ์ด๋“ , ๊ธฐ๋ณธ์ ์œผ๋กœ ๊ฐœ๋ฐœํ•ด์•ผํ•˜๋Š” ์นœ๊ตฌ๋“ค ๊ฐ™๋‹ค. (์• ํ”Œ์—์„œ๋Š” Collection View๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์„ค๋ช…ํ•˜์ง€๋งŒ, ๋‚˜๋Š” Table View ๊ธฐ๋ฐ˜์œผ๋กœ ์„ค๋ช…ํ•˜๊ณ  ์ดํ•ดํ•  ์˜ˆ์ •์ด๋‹ค)

UITableView DataSource

TableView๋ฅผ ๊ตฌํ˜„ํ•  ๋•Œ, ์šฐ๋ฆฌ๋Š” UITableViewDataSource๋ผ๋Š” ํ”„๋กœํ† ์ฝœ์„ ์ฑ„ํƒํ•˜๊ฒŒ ๋œ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๊ทธ ํ”„๋กœํ† ์ฝœ์€ ํ•ญ์ƒ ์–˜๋„ค๋“ค์„ ๊ตฌํ˜„ํ•ด์ค˜์•ผ๋œ๋‹ค.

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
	// ๋ช‡ ๊ฐœ์˜ Cell์„ ๋ณด์—ฌ์ค„๊ฑฐ์•ผ?
}
   
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
	// ์–ด๋–ค Cell์„ ๋ณด์—ฌ์ค„๊ฑฐ์•ผ?
}

๊ต‰์žฅํžˆ ์ง๊ด€์ ์ด๋‹ค. ๋ช‡ ๊ฐœ์˜ Cell์„, ์–ด๋–ค ๋ชจ์–‘์œผ๋กœ ๋ณด์—ฌ์ค„ ์ง€ ์•Œ๋ ค์ฃผ๊ธฐ๋งŒ ํ•˜๋ฉด, TableView๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค.


์‹ค์ œ ์•ฑ์—์„œ ์–ด๋–ป๊ฒŒ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋Š”๊ฐ€?

๋‹ค์Œ์€ ๋‚ด๊ฐ€ ์ง„ํ–‰ํ•˜๊ณ  ์žˆ๋Š” ํ”„๋กœ์ ํŠธ ShortcutsZip์ด๋‹ค.(์–˜๋Š” SwiftUI์ž„)

ShortcutsZip ํ™”๋ฉด1
ShortcutsZip ํ™”๋ฉด2

๋‹จ์ถ•์–ด(Shortcut์ด๋ผ๋Š” ๊ตฌ์กฐ์ฒด)๋“ค์„ ๋‹ค์–‘ํ•œ ํ˜•ํƒœ๋กœ ๋ถ„๋ฅ˜ํ•ด์„œ, ์‚ฌ์šฉ์ž๋“ค์ด ๋” ์‰ฝ๊ฒŒ ๋‹จ์ถ•์–ด๋ฅผ ์ฐพ์„ ์ˆ˜ ์žˆ๋„๋ก ๊ตฌ์„ฑ ๋ผ ์žˆ๋‹ค.

์ด๋ ‡๊ฒŒ ๋‹ค์–‘ํ•œ ์ •๋ณด๋ฅผ, ๋˜๋Š” ๊ฐ™์€ ๋ฐ์ดํ„ฐ ํƒ€์ž…์ด์ง€๋งŒ ํŠน์ • ์นดํ…Œ๊ณ ๋ฆฌ๋กœ ๋‚˜๋ˆ ์„œ ์ •๋ณด๋ฅผ ์ œ๊ณตํ•˜๊ณ  ์‹ถ์„ ๋•Œ ์šฐ๋ฆฌ๋Š” Section ์ด๋ผ๋Š” ๊ฐœ๋…์„ DataSource์—๊ฒŒ ์ œ๊ณตํ•ด์ค˜์•ผํ•œ๋‹ค.

func numberOfSections(in tableView: UITableView) -> Int {
    // ๋ช‡ ๊ฐœ์˜ Section์„ ๊ฐ€์ง€๊ณ  ์žˆ์–ด?
}

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    // ์„น์…˜๋งˆ๋‹ค ๋ฐ์ดํ„ฐ๊ฐ€ ๋ช‡ ๊ฐœ ๋“ค์–ด์žˆ์–ด?
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    // Cell์˜ ๋ชจ์–‘์€ ์–ด๋–ป๊ฒŒ ์ƒ๊น€?
}

๋‚ด๊ฐ€ ์ง„ํ–‰ํ–ˆ๋˜ ShortcutsZip์˜ ๊ฒฝ์šฐ, ์ด๋Ÿฐ์‹์œผ๋กœ ํ‘œํ˜„์„ ํ•  ์ˆ˜ ์žˆ๋‹ค.

var shortcutDataList = [[Shortcut]]()

/* ์ƒ๋žต */

func numberOfSections(in tableView: UITableView) -> Int {
	shortcutDataList.count // 2์ฐจ์› ๋ฐฐ์—ด์˜ count ๊ฐ’์ด Section์˜ ๊ฐœ์ˆ˜
}

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  shortcutDataList[section].count // ๋ฐฐ์—ด์˜ section ์ธ๋ฑ์Šค์— ๋“ค์–ด์žˆ๋Š” ๋ฐ์ดํ„ฐ์˜ ๊ฐœ์ˆ˜๊ฐ€ Cell์˜ ๊ฐœ์ˆ˜
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
		// Section๋งˆ๋‹ค Cell ๋ชจ์–‘์„ ์ง€์ •ํ•ด์„œ ๋ฆฌํ„ด
    let section = indexPath.section 
		switch section {
			let cell = UITableViewCell() 
			return cell
		}
}

๊ทธ๋Ÿฐ๋ฐ, ์šฐ๋ฆฌ๊ฐ€ ์‹ค์ œ๋กœ ๊ฐœ๋ฐœํ•  ๋• 1์ฐจ์› ๋ฐฐ์—ด๋กœ ํ‘œํ˜„ํ•˜๊ธฐ ์–ด๋ ค์šด ๊ฒฝ์šฐ๊ฐ€ ๊ต‰์žฅํžˆ ๋งŽ๋‹ค. ๋ฌด์‹ ์‚ฌ ์•ฑ์„ ๋ด ๋ณด์ž. (ํ…Œ์ด๋ธ” ๋ทฐ๋Š” ์•„๋‹˜)

๋ฌด์‹ ์‚ฌ ๋ฉ”์ธํ™”๋ฉด1
๋ฌด์‹ ์‚ฌ ๋ฉ”์ธํ™”๋ฉด2

ShortcutsZip๊ณผ ๋‹ค๋ฅด๊ฒŒ, ๋ฉ”์ธํ™”๋ฉด์—์„œ ๋‹ค์–‘ํ•œ ์ •๋ณด๋ฅผ ๋ณด์—ฌ์ฃผ๊ณ  ์žˆ๋‹ค. ๊ฐ ์„น์…˜๋งˆ๋‹ค Clothes๋ผ๋Š” Struct๊ฐ€ ์•„๋‹ˆ๋ผ, ๊ด‘๊ณ , ์นดํ…Œ๋กœ๊ธฐ, ๋ผ์ด๋ธŒ ํŽธ์„ฑํ‘œ, ์˜ท ๋“ฑ ๋‹ค์–‘ํ•œ Struct๋กœ ๊ตฌ์„ฑ๋œ ์ •๋ณด๋“ค์„ ๋ณด์—ฌ์ฃผ๊ณ  ์žˆ๋‹ค.

๊ทธ๋Ÿฐ๋ฐ, ๋ฌด์‹ ์‚ฌ์™€ ๊ฐ™์ด ์—ฌ๋Ÿฌ ๊ฐœ์˜ ๋ฐ์ดํ„ฐ๋ฅผ DataSource์— ์–ด๋–ป๊ฒŒ ํ‘œํ˜„ํ•˜๊ฒŒ ๋ ๊นŒ๋ฅผ ์ƒ๊ฐํ•ด๋ณด์ž. (์ผ์ฐจ์›์ ์œผ๋กœ๋งŒ) ๋Œ€์ถฉ ์ด๋ ‡๊ฒŒ ์ƒ๊ฒผ๊ฒ ์ง€??


var advertiseList: [AdverTise] = [] // ๊ด‘๊ณ  ํƒญ
var categories: [Category] = [] // ์นดํ…Œ๊ณ ๋ฆฌ ๋ฆฌ์ŠคํŠธ
var liveContents: [Contents] = [] // ๋ผ์ด๋ธŒ ๋ฐฉ์†ก
/* ๊ธฐํƒ€ ์ •๋ณด๋“ค .. */

/* (์ƒ๋žต)์„œ๋ฒ„ ๋ฐ์ดํ„ฐ ํ˜ธ์ถœ -> ๊ฐ ์ •๋ณด๋“ค ์—…๋ฐ์ดํŠธํ•˜๋Š” ๋กœ์ง๋“ค */

func numberOfSections(in tableView: UITableView) -> Int {
	// section์˜ ๊ฐœ์ˆ˜ return
    // ์„น์…˜ ๊ฐœ์ˆ˜๋ฅผ ์ง์ ‘ ๋ฆฌํ„ดํ•˜๊ฑฐ๋‚˜ UI๋ฅผ ์„œ๋ฒ„๋กœ ๊ฒฐ์ •ํ•˜๋ฉด ๊ทธ ๋ฐ์ดํ„ฐ์˜ count๋ฅผ return)
}

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  switch section {
		case 0: 
			return advertiseList.count
		case 1:
			return categories.count
		case 2:
			return liveContents.count
		/* ์ƒ๋žต */
	}	
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let section = indexPath.section
		switch section {
			let cell = UITableViewCell() 
			return cell
		}

}

์•„์ง๊นŒ์ง€ dataSource๋Š” ๊ต‰์žฅํžˆ ์ง๊ด€์ ์ด๋‹ค. ๋‚ด๊ฐ€ ๊ฐ€์ง„ Section ๊ฐœ์ˆ˜, Section ๋‚ด๋ถ€ ๋ฐ์ดํ„ฐ ๊ฐœ์ˆ˜, Cell์˜ ๋ชจ์–‘๋งŒ ์•Œ๋ ค์ค€๋‹ค๋ฉด, ์•Œ์•„์„œ TableView๋ฅผ ๊ทธ๋ฆด ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

๊ทธ๋Ÿผ ์–ด๋–ค ๋ฌธ์ œ๊ฐ€ ์žˆ๋Š”๋ฐ?

์šฐ์„  ์‹ค์ œ ์•ฑ์€ ๋‹จ์ˆœํžˆ 1~2์ฐจ์›์˜ ๋ฐฐ์—ด๋กœ ํ‘œํ˜„ํ•  ์ˆ˜ ์—†๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ๋‹ค.

์—ฌ๊ธฐ์„œ ๊ฐ€์žฅ ํฐ ๋ฌธ์ œ๋Š” ์‚ฌ์šฉ์ž์˜ ์ž…๋ ฅ์œผ๋กœ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ณ€ํ™”ํ–ˆ์„ ๋•Œ, ๊ฐœ๋ฐœ์ž๊ฐ€ ์ง์ ‘ UI์—๋„ ์—…๋ฐ์ดํŠธ๋ฅผ ์‹œ์ผœ์ค˜์•ผํ•œ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค. (๋ฐ์ดํ„ฐ๊ฐ€ ๋ณ€ํ™”ํ•œ๋‹ค๊ณ , ์•Œ์•„์„œ TableView๊ฐ€ ๋ณ€ํ•˜์ง€ ์•Š๋Š”๋‹ค. ์šฐ๋ฆฌ๋Š” reloadData ๋“ฑ์„ ์‚ฌ์šฉํ•ด์•ผํ•œ๋‹ค๋Š” ๊ฒƒ์„ ์˜๋ฏธํ•œ๋‹ค.)

์กฐ๊ธˆ ๋” ์ž์„ธํ•œ ์˜ˆ๋กœ ๋ฌด์‹ ์‚ฌ ์•ฑ์—์„œ ์–ด๋–ค ์˜ท์— ์ข‹์•„์š”๋ฅผ ๋ˆ„๋ฅธ ์ƒํ™ฉ์„ ์ƒ๊ฐํ•ด๋ณด์ž. ๊ทธ๋Ÿผ ๋Œ€์ถฉ ์ด๋Ÿฐ ๋กœ์ง์œผ๋กœ ๋™์ž‘ ํ•  ๊ฒƒ์ด๋‹ค.ใ…‹

  1. ์‚ฌ์šฉ์ž๊ฐ€ ์ข‹์•„์š” ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅธ๋‹ค.

  2. ์„œ๋ฒ„์— Post๋ฅผ ๋ณด๋ƒ„ (ใ…‡ใ…‡๊ฐ€ ์ข‹์•„์š” ๋ˆŒ๋ €์–ด)

  3. ์„œ๋ฒ„๊ฐ€ ์‘๋‹ต์„ ๋ณด๋‚ด์คŒ

  4. ์‘๋‹ต์„ ๊ธฐ๋ฐ˜์œผ๋กœ ๋ฐ์ดํ„ฐ์™€ UI ์—…๋ฐ์ดํŠธํ•œ๋‹ค.

์šฐ๋ฆฌ๋Š” ์ด๋•Œ, ์ž์—ฐ์Šค๋Ÿฌ์šด ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ๋ณด์—ฌ์ฃผ๊ธฐ ์œ„ํ•ด์„œ reloadData()๋ฅผ ์ˆ˜ํ–‰ํ•œ ํ›„์— scroll์˜ ์œ„์น˜๋ฅผ ๋ฐ”๊ฟ”์ฃผ๊ธฐ๋„ ํ•˜๊ณ , insertRow(at:)์„ ํ†ตํ•ด์„œ ๋ฐ์ดํ„ฐ์˜ ๋ณ€ํ™”์™€ UI์˜ ๋ณ€ํ™”๋ฅผ ์—ฐ๊ฒฐ์‹œ์ผœ์ฃผ๊ธฐ๋„ ํ•œ๋‹ค. ์ฆ‰, ์ด์ œ๊นŒ์ง€ ํ–ˆ๋˜ ์ž‘์—…๋“ค์ด ๊ฐœ๋ฐœ์ž๋“ค์ด ์ง์ ‘ ๋ณ€๊ฒฝ๋œ ๋ฐ์ดํ„ฐ์˜ ์œ„์น˜๋ฅผ ์ฐพ์•„์„œ! ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ๋ณด์—ฌ์คฌ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค. (๋งž๊ฒ ์ง€?)

๊ทธ๋ฆฌ๊ณ  ๊ฐ€์žฅ ํฌ๋ฆฌํ‹ฐ์ปฌํ•œ ๋ฌธ์ œ! ๊ฐœ๋ฐœํ•˜๋Š” ์ค‘, synchronization์—๋Ÿฌ๋ฅผ ๋งŽ์ด ๊ฒฝํ—˜ํ–ˆ์„ ๊ฒƒ์ด๋‹ค. ๊ฐ„๋‹จํ•œ ์˜ˆ์‹œ๋กœ, Section์ด 3๊ฐœ์ธ TableView๊ฐ€ ์žˆ๋‹ค๊ณ  ๊ฐ€์ •ํ•˜์ž. ์ด๋•Œ, 3๋ฒˆ์งธ Section์— ๋ฐ์ดํ„ฐ๊ฐ€ 1๊ฐœ ์žˆ์—ˆ๋Š”๋ฐ ๋งˆ์ง€๋ง‰ ๋‚จ์€ ๋ฐ์ดํ„ฐ ํ•˜๋‚˜๋ฅผ ์ง€์šด ๊ฒƒ์ด๋‹ค. ๊ทธ๋Ÿผ Section์—๋Š” ์•„๋ฌด๊ฒƒ๋„ ์—†๊ฒŒ ๋œ๋‹ค. ์ด๋•Œ, ๋„ˆ๊ฐ€ dataSource์—์„œ ์•Œ๋ ค์ค€ ๊ฐ’์ด๋ž‘, ๋‚ด๊ฐ€ ๋ณด์—ฌ์ค˜์•ผ๋˜๋Š” ๊ฐ’์ด ๋‹ค๋ฅธ๋ฐ? ์ด๋Ÿฌ๋ฉด์„œ ์•ฑ์ด ์ฃฝ๊ฒŒ ๋˜๋Š”๊ฒƒ์ด๋‹ค ^^^^

Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: Invalid update: invalid number of sections. The number of sections contained in the collection view after the update (10) must be equal to the number of sections contained in the collection view before the update (10), plus or minus the number of sections inserted or
deleted (0 inserted, 1 deleted).'

์• ํ”Œ์€ ์ด๋ ‡๊ฒŒ ์ž์—ฐ์Šค๋Ÿฌ์šด ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ๋ณด์—ฌ์ฃผ๋Š”๊ฒŒ ์–ด๋ ค์šด ์ƒํ™ฉ, ๊ทธ๋ฆฌ๊ณ  synchronization ์—๋Ÿฌ ๋“ฑ์ด dataSource์— ์žˆ๋‹ค๊ณ  ๋งํ–ˆ๋‹ค. ์ฆ‰, UI๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋Š” ์ฃผ์ฒด = Data! ๊ทธ๋Ÿฐ๋ฐ, Data๊ฐ€ ๋ณ€ํ™”ํ•ด๋„ ๊ฐœ๋ฐœ์ž๊ฐ€ ์•Œ๋ ค์ฃผ์ง€ ์•Š์œผ๋ฉด, UI๊ฐ€ ์—…๋ฐ์ดํŠธ ๋˜์ง€ ์•Š๋Š” ์ด๊ฒƒ๋“ค์ด ๋ชจ์—ฌ ์ €๋Ÿฐ ๋ฌธ์ œ๋ฅผ ๋ฐœ์ƒ์‹œํ‚จ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค.

๊ทธ๋ž˜์„œ ๋ฐ์ดํ„ฐ๊ฐ€ ์—…๋ฐ์ดํŠธ ๋˜๋ฉด, UI๋ฅผ ์—…๋ฐ์ดํŠธ ์‹œํ‚ค๋ฉด ๋˜์ง€ ์•Š์„๊นŒ? ์—์„œ ํƒ„์ƒํ•œ ๊ฒƒ์ด ๋ฐ”๋กœ Diffable DataSource์ธ ๊ฒƒ์ด๋‹ค~!!!~!


์ƒˆ๋กœ์šด Data ๊ด€๋ฆฌ ๋ฐฉ๋ฒ• - Diffable Data Source

Diffable Data Source์˜ ๊ธฐ๋ณธ์ ์ธ ๊ฐœ๋…์€ ๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ์–ตํ•ด ๋‘์—ˆ๋‹ค๊ฐ€, ๋ณ€๊ฒฝ์‚ฌํ•ญ์ด ์žˆ์œผ๋ฉด, UI๋ฅผ ์—…๋ฐ์ดํŠธ ์‹œํ‚ค๋Š” ๊ฒƒ์ด๋‹ค. ์ฆ‰, ๋ฐ์ดํ„ฐ์˜ ๊ด€๋ฆฌ์™€ UI ์—…๋ฐ์ดํŠธ๋ฅผ ๋ถ„๋ฆฌ์‹œํ‚ค์ง€ ์•Š๊ฒ ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค.

๊ทธ๋ ‡๋‹ค๋ฉด, ์ด์ „ ๋ฐ์ดํ„ฐ๋ฅผ ์–ด๋–ป๊ฒŒ ๊ธฐ์–ตํ• ๊นŒ? โ†’ ์—ฌ๊ธฐ์„œ ๋‚˜ํƒ€๋‚˜๋Š” ๊ฐœ๋…์ด snapshot์ด๋‹ค.

์šฐ๋ฆฌ๊ฐ€ snapshot()์ด๋ผ๋Š” ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๊ทธ ์ˆœ๊ฐ„, DataSource์— ์ €์žฅ๋˜์–ด์žˆ๋˜ ๋ฐ์ดํ„ฐ๋“ค์„ commit ํ•œ๋‹ค. ๊ทธ๋‹ค์Œ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ณ€๊ฒฝ๋œ ํ›„ ๋‹ค์‹œ ํ•œ ๋ฒˆ snapshot()์„ ํ˜ธ์ถœํ•˜๊ฒŒ ๋˜๋ฉด, ์ด์ „ snapshot๊ณผ ๋น„๊ตํ•ด์„œ ์–ด๋–ค ๋ฐ์ดํ„ฐ๊ฐ€ ๋‹ฌ๋ผ์กŒ๋Š”์ง€ ํŒŒ์•…ํ•˜๊ณ  ๊ทธ๊ฒƒ์„ ์• ๋‹ˆ๋ฉ”์ด์…˜์œผ๋กœ ๋ณด์—ฌ์ฃผ๋Š” ๊ฒƒ์ด๋‹ค. (option์„ ์ด์šฉํ•ด์„œ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ๋Œ ์ˆ˜ ์žˆ๋‹ค)

dataSource๋Š” ๋ฐ์ดํ„ฐ์˜ ๋ณ€๊ฒฝ์‚ฌํ•ญ์„ ์–ด๋–ป๊ฒŒ ์ธ์ง€ํ•  ์ˆ˜ ์žˆ์„๊นŒ? โ†’ ์ด๋ฅผ ์œ„ํ•ด ์šฐ๋ฆฌ๋Š” Item์— ๋ฐ˜๋“œ์‹œ Hashable ํ”„๋กœํ† ์ฝœ์„ ์ฑ„ํƒํ•ด์ค˜์•ผํ•œ๋‹ค. ์ฆ‰, Diffable Data Source๋Š” ์ด์ „ ๋ฐ์ดํ„ฐ๋“ค์˜ ID๋ฅผ ๊ธฐ์–ตํ•ด๋‘๊ณ , ๊ทธ ๊ฐ’๋“ค์ด ๋ณ€ํ•˜๋ฉด ๋ฐ์ดํ„ฐ๊ฐ€ ๋ณ€ํ–ˆ์Œ์„ ์ธ์‹ํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค!

์ด์ œ ์šฐ๋ฆฌ๋Š” performBatchUpdates ์—†์ด ๋ชจ๋“  ๋ณ€๊ฒฝ์‚ฌํ•ญ์— ๋Œ€ํ•œ ์ฑ…์ž„์„ DataSource์—๊ฒŒ ์œ„์ž„ํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  DiffableDataSource๋Š” ์•„์ฃผ ์ž์—ฐ์Šค๋Ÿฝ๊ณ  ์˜ˆ์œ Animation์„ ๋ณด์—ฌ์ฃผ๊ฒŒ ๋œ๋‹ค.

DiffableDataSource๋Š” UITableView, UICollectionView ๋ชจ๋‘ ์ง€์›ํ•˜๋ฉฐ, Mac, TVOS, iOS๋ฅผ ์ง€์›ํ•œ๋‹ค. (iOS 13๋ถ€ํ„ฐ ๊ฐ€๋Šฅ)


์•ฑ์„ ํ†ตํ•œ ๋ฐ๋ชจ (์‚ฌ์šฉ ๋ฐฉ๋ฒ•)

๐Ÿ’ก ๋‚ด ๋งˆ์Œ๋Œ€๋กœ ์„ค๋ช…ํ• ๊ฑฐ์ž„.. WWDC ์˜์ƒ ๋ณด๋Š” ๊ฑฐ ์ถ”์ถด ๐Ÿ’ก

์š”์•ฝํ•˜์ž๋ฉด ์„ธ ๋‹จ๊ณ„๋กœ ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ์„ ์‰ฝ๊ฒŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.

  1. Diffable DataSource ๋“ฑ๋ก

  2. Snapshot ๋งŒ๋“ค๊ธฐ

  3. Snapshot ์ ์šฉ

์ด์ œ ์ž์„ธํ•˜๊ฒŒ ์•Œ์•„๋ณด์ž.

  1. Diffable DataSource ๋“ฑ๋ก

    UITableVIew๋‚˜ UICollectionView๋ฅผ ๋งŒ๋“ค ๋•Œ, ๊ธฐ๋ณธ DataSource๋ฅผ ๋“ฑ๋กํ•˜๊ฒŒ ๋œ๋‹ค. ์ด๋•Œ DiffableDataSource์— ๊ธฐ๋ณธ ๋ฐ์ดํ„ฐ ๊ฐ’์„ ๋„ฃ์–ด์ค€๋‹ค.

  2. ๋ณ€๊ฒฝ ์‚ฌํ•ญ์ด ์ƒ๊ธด ๊ฒฝ์šฐ Snapshot์„ ๋งŒ๋“ค๊ณ , Snapshot์„ ์ ์šฉํ•œ๋‹ค.

    ๊ธฐ์กด PrefetchData์™€ ๋น„์Šทํ•œ ํ˜•ํƒœ์ด๋‹ค. ์ฆ‰, ๊ธฐ์กด ๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ์ค€์œผ๋กœ snapshot์„ ์ƒ์„ฑํ•˜๊ณ , data์˜ ๋ณ€๊ฒฝ์‚ฌํ•ญ์„ ์ฒ˜๋ฆฌํ•œ ํ›„์— snapshot์„ ์ ์šฉ (apply)ํ•ด์ฃผ๊ธฐ๋งŒ ํ•˜๋ฉด UI๊ฐ€ ์•Œ์•„์„œ ๋ณ€๊ฒฝ๋˜๋Š” ํ˜•ํƒœ์ด๋‹ค.


// ํ™”๋ฉด์— ๋ณด์—ฌ์ค„ Item - Hashable protocol ์ฑ„ํƒ ํ•„์š”
struct Shortcut: Hashable {
	var title: String
	var category: String
}

class ViewController: UIViewController {
    
    enum Section {
        case category
        case popular
        case latest
        
        var headerTitle: String {
            switch self {
            case .category:
                return "์นดํ…Œ๊ณ ๋ฆฌ ๋ณ„ ๋‹จ์ถ•์–ด"
            case .popular:
                return "์ธ๊ธฐ์žˆ๋Š” ๋‹จ์ถ•์–ด"
            case .latest:
                return "์ตœ์‹  ๋‹จ์ถ•์–ด"
            }
        }
    }
    
    // diffable dataSource ์„ ์–ธ
    private var dataSource: UITableViewDiffableDataSource<Section, Shortcut>!
    
    @IBOutlet weak var tableView: UITableView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupDataSource()
        setSnapshot()
        
    }
    
    // dataSource ๋“ฑ๋ก
    private func setupDataSource() {
        
        dataSource = UITableViewDiffableDataSource<Section, Shortcut>(tableView: tableView) { tableView, indexPath, item in
            guard let cell = tableView.dequeueReusableCell(withIdentifier: ShortcutTableViewCell.id, for: indexPath) as? ShortcutTableViewCell else { return UITableViewCell() }
            cell.config(with: item)
            return cell
        }
    }
    
    private func setSnapshot()  {
        let category = [
            Shortcut(title: "๋‹จ์ถ•์–ด1", subTitle: "๋‹จ์ถ•์–ด๋ฅผ ์จ๋ณด์„ธ์š”"),
            Shortcut(title: "๋‹จ์ถ•์–ด2", subTitle: "์‰ฝ๊ฒŒ ์จ ๋ณด์ž"),
            Shortcut(title: "๋‹จ์ถ•์–ด3", subTitle: "๊ฟ€๋„ ์ข€ ๋นจ์•„๋ณด์ž")
        ]
        
        let popular = [
            Shortcut(title: "๋‹จ์ถ•์–ด4", subTitle: "๋‹จ์ถ•์–ด๋ฅผ ์จ๋ณด์„ธ์š”"),
            Shortcut(title: "๋‹จ์ถ•์–ด5", subTitle: "์‰ฝ๊ฒŒ ์จ ๋ณด์ž"),
            Shortcut(title: "๋‹จ์ถ•์–ด6", subTitle: "๊ฟ€๋„ ์ข€ ๋นจ์•„๋ณด์ž")
        ]
        
        let latest = [
            Shortcut(title: "๋‹จ์ถ•์–ด7", subTitle: "๋‹จ์ถ•์–ด๋ฅผ ์จ๋ณด์„ธ์š”"),
            Shortcut(title: "๋‹จ์ถ•์–ด8", subTitle: "์‰ฝ๊ฒŒ ์จ ๋ณด์ž"),
            Shortcut(title: "๋‹จ์ถ•์–ด9", subTitle: "๊ฟ€๋„ ์ข€ ๋นจ์•„๋ณด์ž")
        ]
        
        // snapshot ์ ์šฉ
        var snapshot = dataSource.snapshot()
        snapshot.appendSections([.category, .latest, .popular])
        snapshot.appendItems(category, toSection: .category)
        snapshot.appendItems(popular, toSection: .popular)
        snapshot.appendItems(latest, toSection: .latest)
        dataSource.apply(snapshot)
    }
}
	

Diffable DataSource๋ฅผ ํ™œ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•

  • snapshot ์ƒ์„ฑ ๋ฐฉ๋ฒ•์—๋Š” 2๊ฐ€์ง€๊ฐ€ ์žˆ๋‹ค. - ๊ธฐ์กด dataSource๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ snapshot์„ ์ƒ์„ฑํ•˜๋Š” ๋ฐฉ๋ฒ• - ์ƒˆ๋กœ์šด snapshot์„ ์ƒ์„ฑํ•˜๋Š” ๋ฐฉ๋ฒ•

์šฐ๋ฆฌ๋Š” ์—ฌ๊ธฐ์„œ ์™œ ๋‘๊ฐ€์ง€ ์ƒ์„ฑ๋ฐฉ๋ฒ•์ด ์žˆ์„๊นŒ? ๋ฅผ ๋ฐ˜๋“œ์‹œ ์ƒ๊ฐํ•ด๋ด์•ผํ•œ๋‹ค. ์—ฌ๊ธฐ์„œ ์ ค ์ค‘์š”ํ•œ ๊ฐœ๋…์€ ์ด๊ฑฐ๋‹ค.

๐Ÿ’ก diffableDataSource๋Š” snapshot์„ ๋งŒ๋“ค ๋•Œ, ๋ณต์‚ฌ๋ณธ์„ return ํ•œ๋‹ค.

๋ถ„๋ช… ์•„๊นŒ ์„ค๋ช…ํ•  ๋•, diffable data source๋ฅผ ๋งŒ๋“ค๊ฒŒ ๋˜๋ฉด, viewController์—๋Š” ์›๋ณธ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์„ ํ•„์š”๊ฐ€ ์—†๋‹ค๊ณ  ์„ค๋ช…ํ–ˆ์—ˆ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ, snapshot์„ ๋งŒ๋“ค ๋•Œ ๋ณต์‚ฌ๋ณธ์„ return ํ•œ๋‹ค๊ณ ? ๋ง๋„ ์•ˆ๋˜๋Š” ์ƒํ™ฉ์ธ๊ฑฐ๋‹ค. (์ด๊ฑฐ ๋•Œ๋ฌธ์— ์ง„์งœ ์‚ฝ์งˆ ์—„์ฒญ ๋งŽ์ด ํ–ˆ๋‹ค)

์ดํ•ด๊ฐ€ ์•ˆ๋  ์ˆ˜๋„ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์กฐ๊ธˆ ๋” ์„ค๋ช…์„ ํ•ด๋ณด์ž๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

๋ฌด์‹ ์‚ฌ์—์„œ ์–ด๋–ค ์˜ท์— ๋Œ€ํ•ด ์ข‹์•„์š”๋ฅผ ๋ˆŒ๋ €๋‹ค๊ณ  ์ƒ๊ฐํ•ด๋ณด์ž. ์ข‹์•„์š”๋Š” ID ๊ฐ’์ด ์•„๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์–ด์ง€๊ฑฐ๋‚˜ ์ƒ์„ฑ๋˜๋Š” ์กฐ๊ฑด์ด ์•„๋‹ˆ๊ณ , ์†์„ฑ์˜ ์ผ๋ถ€๋ถ„์ด ์ˆ˜์ •๋œ ๊ฒฝ์šฐ๊ฐ€ ๋˜๋Š” ๊ฒƒ์ด๋‹ค.

๋‹ค์‹œ ์ƒ๊ฐํ•ด๋ณด์ž. ์šฐ๋ฆฌ๋Š” dataSource์˜ snapshot์„ ๊ฐ€์ง€๊ณ  ์™€์„œ ๊ทธ snapshot์—์„œ ๋ณ€๊ฒฝ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์ฒ˜๋ฆฌํ–ˆ์—ˆ๋‹ค. ๊ทผ๋ฐ ์ด๊ฒŒ ์›๋ณธ ๋ฐ์ดํ„ฐ๊ฐ€ ์•„๋‹ˆ๋ผ โ€œ๋ณต์‚ฌ๋ณธโ€์ด๊ธฐ ๋•Œ๋ฌธ์—, ์•„๋ฌด๋ฆฌ ๋ณต์‚ฌ๋ณธ์„ ๋ณ€ํ™”์‹œํ‚จ๋‹ค๊ณ  ํ•ด๋„, ๊ทธ ๋ณ€๊ฒฝ์ด ๋ณด์ด์ง€ ์•Š๋Š” ๊ฒƒ์ด๋‹ค.

์ฆ‰, ๊ธฐ์กด dataSource์˜ snapshot์„ ๊ธฐ๋ฐ˜์œผ๋กœ ์•„๋ฌด๋ฆฌ ์†์„ฑ์„ ๋ณ€๊ฒฝํ•ด๋ด๋„, ๋ฐ์ดํ„ฐ๊ฐ€ ๋ณ€ํ•˜์ง€ ์•Š๋Š”๋‹ค๋Š”๊ฒƒ์ด๋‹ค.

๊ทธ๋Ÿผ ์–ด๋–ป๊ฒŒํ•˜๋ฉด๋˜๋Š”๋ฐ~!!!!!

๋ฐ์ดํ„ฐ๋ฅผ ์‚ญ์ œํ•˜๊ณ , ๋‹ค์‹œ ๋„ฃ์–ด๋ผ~! ๋ผ๋Š”๊ฒŒ Apple์˜ ๋‹ต๋ณ€์ธ ๋“ฏ ํ•˜๋‹ค ^^

์ด๋ฅผ ์œ„ํ•ด insertItem(from: ) ์ด๋ผ๋Š” ํ•จ์ˆ˜๊ฐ€ ์กด์žฌํ•œ๋‹ค.

์ฆ‰, ์‚ฌ์šฉ์ž๊ฐ€ A๋ผ๋Š” ์˜ท์— ์ข‹์•„์š”๋ฅผ ๋ˆŒ๋ €๋‹ค๋ฉด, A๋ฐ์ดํ„ฐ๋ฅผ ์‚ญ์ œํ–ˆ๋‹ค๊ฐ€ insertItem์„ ๊ธฐ๋ฐ˜์œผ๋กœ ์›์œ„์น˜์— ์‚ฝ์ž…์‹œํ‚จ๋‹ค๋ฉด, ์• ํ”Œ์ด ์•Œ์•„์„œ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ๋ณด์—ฌ์ฃผ๊ฒ ๋‹ค~ ๋ผ๋Š” ๊ฒƒ์ด๋‹ค.

๊ทธ ์ด์™ธ์—๋„ insert(before)์ด๋ผ๋Š” ํ•จ์ˆ˜๋„ ์žˆ์œผ๋‹ˆ ๋ฐ์ดํ„ฐ ์ˆœ์„œ ๊ณ ๋ คํ•ด์„œ ๋„ˆ๋„ค๊ฐ€ ์•Œ์•„์„œ ์ž‘์„ฑํ•ด~ ๋Œ€์‹  insert(before: )๋Š” ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์„ ๋•Œ append๋กœ ์ž‘๋™ํ•˜๊ฒŒ ๋งŒ๋“ค์–ด๋’€์–ด~~~~ ๋ผ๋Š”๊ฒŒ ์š”์•ฝ~

indexPath

์ด์ œ ์šฐ๋ฆฌ๋Š” ViewController์— ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์ง€ ์•Š๋‹ค. diffableDataSource๊ฐ€ ๊ทธ ์ •๋ณด๋ฅผ ๋ชจ๋‘ ๊ฐ€์ง€๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ๊ทธ๋ž˜๋„ ์•„์ง ์šฐ๋ฆฌ๋Š” indexPath์— ๋Œ€ํ•œ ์ •๋ณด๋ฅผ ์•Œ์•„์•ผํ•œ๋‹ค. ์™œ๋ƒ! ์–ด๋–ค Cell์„ ํƒญ ํ–ˆ์„ ๋•Œ, ๋‹ค๋ฅธ ๋ทฐ๋กœ ์ด๋™ํ•ด์•ผ๋˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ์ฆ‰, UITableViewDelegate๋ฅผ ๊ตฌํ˜„ํ•  ๋•Œ, indexPath์ •๋ณด๊ฐ€ ํ•„์š”ํ•˜๋‹จ ๊ฒƒ์ด๋‹ค.

์ด์ „์— Item์— Hashable ํ”„๋กœํ† ์ฝœ์„ ์ง€์ •ํ–ˆ๊ณ , ๊ทธ๋ ‡๊ธฐ ๋–„๋ฌธ์— ๊ทธ Item์„ ์ฐพ๋Š”๋ฐ ์„ ํ˜•์‹œ๊ฐ„์˜ ํƒ์ƒ‰ ์‹œ๊ฐ„์ด ๊ฑธ๋ฆฐ๋‹ค. ์ฝ”๋“œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค. (optional๋กœ return ํ•˜๊ธฐ ๋•Œ๋ฌธ์— guard let ์‚ฌ์šฉ ์ถ”์ฒœ!)

let item = dataSource.itemIdentifier(for: indexPath)

๊ฐœ๋ฐœํ•˜๋ฉฐ ๊ณ ๋ คํ•ด์•ผํ•  ์  (๊ทธ๋ƒฅ ์ž๋ž‘์ธ ๋“ฏ)

  • apply()๋Š” BackgroundQueue์—์„œ ๋™์ž‘ํ•˜๊ธฐ ๋•Œ๋ฌธ์—, mainQueue๋กœ ๊ฐ€์ ธ์™€์„œ ์‹คํ–‰ํ•˜์ž

  • AirDrop๋„ DiffableDataSource๋กœ ๋งŒ๋“ ๊ฑฐ์•ผ~ ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋ฉ‹์ง€์ง€?


์˜์ƒ

PreviousUIDiffableDataSourceNext[WWDC2019] Advances in CollectionView Layout

Last updated 1 year ago

๐ŸŽ
Advances in UI Data Sources - WWDC19 - Videos - Apple Developer