iOS/RxSwift

[RxSwift] RxDataSource ์ž…๋ฌธ ํ•ด๋ณด๊ธฐ(feat.MVVM)

๊ฒฝํ‘ธ 2023. 9. 28. 14:30
๋ฐ˜์‘ํ˜•

์•ˆ๋…•ํ•˜์„ธ์š”.

์˜ค๋Š˜์€ RxDataSource์— ๋Œ€ํ•ด์„œ ์ •๋ฆฌํ•ด๋ณด๋ ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

RxDataSource๋ฅผ ํ•™์Šตํ•˜๊ณ  ๊ฐ„๋‹จํ•œ ์˜ˆ์ œ๋ฅผ ๋งŒ๋“ค์–ด๋ดค์Šต๋‹ˆ๋‹ค.

RxDataSource๋ผ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” RxSwift๋ฅผ ๊ณต๋ถ€ํ•˜๊ธฐ ์‹œ์ž‘ํ•  ๋•Œ๋ถ€ํ„ฐ ์•Œ๊ณ ๋Š” ์žˆ์—ˆ์ง€๋งŒ 

ํšŒ์‚ฌ๋‚˜ ๊ฐœ์ธ ํ”„๋กœ์ ํŠธ์—์„œ ์‚ฌ์šฉํ•ด ๋ณธ ์ ์ด ์—†์—ˆ์Šต๋‹ˆ๋‹ค.

์ด๋ฒˆ์— ์—ฌ๋Ÿฌ ๊ฐœ์˜ ์„น์…˜์ด ํ•„์š”ํ•œ ์ž‘์—…์ด ์žˆ์–ด ๊ฐ„๋‹จํ•˜๊ฒŒ ํ•™์Šตํ•˜๊ณ  ์˜ˆ์ œ๋ฅผ ํ†ตํ•ด์„œ ์ž…๋ฌธํ•˜๋Š” ์‹œ๊ฐ„์„ ๊ฐ€์กŒ์Šต๋‹ˆ๋‹ค. 

 

RxDataSource?

๊ธฐ์กด์— ์‚ฌ์šฉํ–ˆ๋˜ ์ฝ”๋“œ๋ฅผ ์‚ดํŽด๋ณด๋ฉด ( RxSwift, RxCocoa ) ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค.

tasks
  .bind(to: taskTableView.rx.items(cellIdentifier: TaskTableViewCell.identifier, cellType: TaskTableViewCell.self)) { (index, item, cell) in
      cell.setDataSource(task: item)
   }
   .disposed(by: disposeBag)

DataSource๋ฅผ ์ฑ„ํƒํ•˜์ง€ ์•Š์•„๋„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์ฝ”๋“œ๋ฅผ ๊ด€๋ฆฌํ•˜๊ธฐ ํŽธํ•œ ์žฅ์ ์„ ๊ฐ€์ง€๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ ์ €์˜ ๊ฒฝ์šฐ์ฒ˜๋Ÿผ ์—ฌ๋Ÿฌ ๊ฐœ์˜ ์„น์…˜์„ ๊ตฌํ˜„ํ•ด์•ผ ํ•œ๋‹ค๋ฉด ์œ„์˜ ์ฝ”๋“œ๋กœ๋Š” ๋™์ž‘์ด ์–ด๋ ต์Šต๋‹ˆ๋‹ค.

์ถ”๊ฐ€์ ์œผ๋กœ ์œ„์˜ ์ฝ”๋“œ๋Š” reloadData๋ฅผ ์ด์šฉํ•ด ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ”์ธ๋”ฉํ•˜๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์—

์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ถ”๊ฐ€ํ•  ์ˆ˜๊ฐ€ ์—†๋Š” ๋‹จ์ ๋„ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.

 

์ด์— ๋Œ€ํ•œ ๋ณด์™„์ ์„ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” ๊ฒƒ์ด ๋ฐ”๋กœ RxDataSource์ž…๋‹ˆ๋‹ค.

๊ธฐ์กด์— rx.items์— ์ ‘๊ทผํ•˜๋ฉด ๋‘ ๊ฐœ์˜ ์ž๋™์™„์„ฑ์ด ๋“ฑ์žฅํ•˜๋Š”๋ฐ

ํ•˜๋‚˜๋Š” cellIdentifier์ด๊ณ  ํ•˜๋‚˜๋Š” dataSource์ž…๋‹ˆ๋‹ค.

์ด dataSource์— ๋“ค์–ด๊ฐ€๋Š” ๊ฒƒ์„ ๊ตฌํ˜„ํ•ด ์œ„์˜ ๋‹จ์ ์„ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

์‚ฌ์šฉ๋ฐฉ๋ฒ•

RxDataSource์—๋Š” UITableview์— ๋Œ€ํ•œ RxTableViewSectionedReloadDataSource๋ฅผ, UICollectionview์—๋Š” RxCollectionViewSectionedReloadDataSource๋ผ๋Š” ํด๋ž˜์Šค๋ฅผ ์ œ๊ณตํ•จ์œผ๋กœ์จ ์„น์…˜์˜ DataSource๋ฅผ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋„์™€์ค๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์— reload๊ฐ€ ์•„๋‹Œ animated๋กœ ๋ณ€๊ฒฝ๋œ ํด๋ž˜์Šค๊ฐ€ ์กด์žฌํ•˜๋Š”๋ฐ ์ด๋Š” ์•ž์„œ ๋งํ•œ ๊ฒƒ์ฒ˜๋Ÿผ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋„์™€์ฃผ๋Š” ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค.

DataSource๊ฐ€ ์–ด๋–ค ํ˜•ํƒœ๋กœ ๋˜์–ด์žˆ๋Š”์ง€ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

let dataSource = RxTableViewSectionedReloadDataSource<SectionModel<String, Item>>(
    configureCell: { dataSource, tableView, indexPath, item in
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
        cell.textLabel?.text = item.title
        return cell
    }
)

SectionModel์ด๋ผ๋Š” ๊ตฌ์กฐ์ฒด๋ฅผ ํ†ตํ•ด ํ•ด๋‹น ์„น์…˜์˜ ๋ชจ๋ธ ๋ฐ์ดํ„ฐ๋ฅผ ์ •์˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋ฌผ๋ก  ๊ธฐ๋ณธ์ ์œผ๋กœ ์ œ๊ณตํ•˜๋Š” ๊ตฌ์กฐ์ฒด์ด๊ธฐ ๋•Œ๋ฌธ์— ์ž์ฒด์ ์œผ๋กœ ์ปค์Šคํ…€ํ•œ ๋ชจ๋ธ์ด ํ•„์š”ํ•˜๋‹ค๋ฉด

public protocol SectionModelType {
    associatedtype Item

    var items: [Item] { get }

    init(original: Self, items: [Item])
}

์œ„์˜ ํ”„๋กœํ† ์ฝœ์„ ์ฑ„ํƒํ•˜๊ณ  ๊ตฌํ˜„ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

์•„๋ž˜๋Š” ์˜ˆ์‹œ ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค.

struct CustomData {
    let id: String
    let name: String
}

struct CustomDataSection {
    var header: String
    var items: [CustomData]
}

extension CustomDataSection: SectionModelType {
    typealias Item = CustomData
    
    init(original: CustomDataSection, items: [CustomData]) {
        self = original
        self.items = items
    }
}

์ด๋ ‡๊ฒŒ ์„น์…˜ ๋ชจ๋ธ์„ ๊ตฌํ˜„ํ–ˆ๋‹ค๋ฉด ์•ž์„œ ์„ค๋ช…ํ•œ DataSource ์ฝ”๋“œ์— ๊ทธ๋Œ€๋กœ ์ ์šฉํ•ด ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

์˜ˆ์ œ: ํ• ์ผ ๋ชฉ๋ก ์•ฑ

TableView์— personal, work, shopping์ด๋ผ๋Š” ์„น์…˜์„ ๊ฐ€์ง„ ํ•  ์ผ ๋ชฉ๋ก์„ ๊ฐ๊ฐ ๊ฐ€์ง€๊ณ  ์žˆ๋Š”

๊ฐ„๋‹จํ•œ ์•ฑ์„ ์˜ˆ์‹œ๋กœ ๋“ค์–ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

์ผ๋‹จ ๊ฐ€์žฅ ๋จผ์ € ํ•  ์ผ ๋ชฉ๋ก ๋ชจ๋ธ์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.

 

Task - ๋ชจ๋ธ

struct Task: IdentifiableType, Equatable {
    let id: Int
    let title: String
    let category: TaskCategory
    
    var identity: Int {
        return self.id
    }
}

enum TaskCategory: String, IdentifiableType {
    case personal
    case work
    case shopping
    
    var identity: String {
        return self.rawValue
    }
}

์•ž์„œ ๋งํ•œ personal, work, shopping ์„ธ ๊ฐ€์ง€์˜ ์„น์…˜๋„ ์—ด๊ฑฐํ˜•์œผ๋กœ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

 

TaskViewModel - ๋ทฐ๋ชจ๋ธ

class TaskViewModel {
    
    private let tasks = BehaviorRelay<[AnimatableSectionModel<TaskCategory, Task>]>(value: [])
    
    func fetchTasks() -> Observable<[AnimatableSectionModel<TaskCategory, Task>]> {
        let personalTasks: [Task] = [
            Task(id: 1, title: "์šฐ์œ  ์‚ฌ๊ธฐ", category: .personal),
            Task(id: 2, title: "์น˜์ฆˆ ์‚ฌ๊ธฐ", category: .personal),
        ]

        let workTasks: [Task] = [
            Task(id: 3, title: "๋ธ”๋กœ๊ทธ ํฌ์ŠคํŒ…", category: .work),
            Task(id: 4, title: "ํšŒ์˜ ์ฐธ์—ฌ", category: .work),
        ]

        let shoppingTasks: [Task] = [
            Task(id: 5, title: "๊ฐ€๋ฐฉ ์‚ฌ๊ธฐ", category: .shopping),
            Task(id: 6, title: "์•„์ดํฐ15 ์‚ฌ๊ธฐ", category: .shopping),
        ]

        let sections: [AnimatableSectionModel<TaskCategory, Task>] = [
            AnimatableSectionModel(model: .personal, items: personalTasks),
            AnimatableSectionModel(model: .work, items: workTasks),
            AnimatableSectionModel(model: .shopping, items: shoppingTasks),
        ]

        tasks.accept(sections)

        return tasks.asObservable()
    }
}

๊ฐ๊ฐ์˜ ์„น์…˜๋ณ„๋กœ ์•„์ดํ…œ์„ ์ƒ์„ฑํ•˜๊ณ  ๋ฐฉ์ถœํ•ฉ๋‹ˆ๋‹ค. ( ์„œ๋ฒ„๋‚˜ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์žˆ๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๊ณ  ๋ฐฉ์ถœํ•ฉ๋‹ˆ๋‹ค. )

AnimatableSectionModel์„ ์‚ฌ์šฉํ•˜์…”๋„ ๋˜๊ณ  SectionModel์„ ์‚ฌ์šฉํ•˜์…”๋„ ๋ฌด๋ฐฉํ•ฉ๋‹ˆ๋‹ค.

 

TaskViewController - ๋ทฐ

private func bind() {
        
    let dataSource = RxTableViewSectionedAnimatedDataSource<AnimatableSectionModel<TaskCategory, Task>>(
        configureCell: { dataSource, tableView, indexPath, item in
            let cell = tableView.dequeueReusableCell(withIdentifier: "TaskCell", for: indexPath) as! TaskCell
            cell.configure(task: item)
            return cell
        },
        titleForHeaderInSection: { dataSource, index in
            return dataSource.sectionModels[index].model.rawValue
        }
    )

    viewModel.fetchTasks()
        .bind(to: tableView.rx.items(dataSource: dataSource))
        .disposed(by: disposeBag)
}

์ด๋ ‡๊ฒŒ ์ž‘์„ฑํ•˜๊ฒŒ ๋˜๋ฉด ์—ฌ๋Ÿฌ๊ฐœ์˜ ์„น์…˜์„ ํ•œ ํ…Œ์ด๋ธ”๋ทฐ์— ํ‘œ์‹œํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

์• ๋‹ˆ๋ฉ”์ด์…˜ ์ฒ˜๋ฆฌ๋„ ํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ์žฅ์ ๋„ ๊ฐ€์ง€๊ณ  ์žˆ์œผ๋‹ˆ

๋‹ค์Œ ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜๋ฉด ์ถ”๊ฐ€, ๋ฆฌ๋กœ๋“œ ๊ทธ๋ฆฌ๊ณ  ์‚ญ์ œ์— ๊ด€ํ•œ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ํ•จ๊ป˜ ๋™์ž‘ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

dataSource.animationConfiguration = AnimationConfiguration(insertAnimation: .fade, reloadAnimation: .fade, deleteAnimation: .fade)

 

 

๊ฐ„๋‹จํ•œ ์˜ˆ์ œ๋ฅผ ํ†ตํ•ด์„œ RxDataSource์— ์ž…๋ฌธํ•ด ๋ดค์Šต๋‹ˆ๋‹ค.

๋„์›€์ด ๋˜์…จ์œผ๋ฉด ์ข‹๊ฒ ์Šต๋‹ˆ๋‹ค.

๊ทธ๋Ÿผ ์ด๋งŒ ๐Ÿ‘‹๐Ÿป ๐Ÿ‘‹๐Ÿป ๐Ÿ‘‹๐Ÿป

 

 

 

 

 

๋ฐ˜์‘ํ˜•