Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
Tags
- 스위프트
- MVVM
- HIG
- tableView
- clean architecture
- uitableview
- Protocol
- 리펙터링
- combine
- Human interface guide
- SWIFT
- ios
- Clean Code
- rxswift
- swiftUI
- ribs
- UITextView
- uiscrollview
- 애니메이션
- Xcode
- Refactoring
- UICollectionView
- RxCocoa
- 리팩토링
- Observable
- 클린 코드
- 리펙토링
- collectionview
- swift documentation
- map
Archives
- Today
- Total
김종권의 iOS 앱 개발 알아가기
[iOS - swift] UITableViewDiffableDataSource 사용 방법 (아이템 업데이트, header, moveRowAt) 본문
iOS 응용 (swift)
[iOS - swift] UITableViewDiffableDataSource 사용 방법 (아이템 업데이트, header, moveRowAt)
jake-kim 2023. 4. 22. 22:46
Diffable DataSoure 사용 개념
* 기본 개념은 이전 포스팅 글 참고
- 데이터 소스 관리
- UI를 업데이트 하는 곳인 ViewController에서 dataSource를 가지고 있고, 이 데이터는 UI에 표시되는 데이터만 사용
- ViewModel을 사용한다면 여기서 실제 items를 들고 있고, 값이 변경될때 ViewController에 모든 아이템들을 전달해주고 업데이트 해달라고 요쳥 시 자동으로 변경해줌
Diffable DataSource가 좋은 이유
- 애니메이션 처리가 안전(크래시를 줄일 수 있음)
- UITableDataSource를 사용하고 변경된 부분에 대해서 애니메이션을 적용하려면 performBatchUpdates()나 beginUpdates()와 endupdates() 사이에 데이터 변경 작업을 작성하여 처리하지만, 이때 타이밍 상 거의 동시에 performBatchUpdates()가 여러곳에서 불리면 index 관련 크래시가 발생
- diffable을 사용하면 index로 접근하는게 아닌 hashable로 접근하기 때문에 index 관련 크래시 발생 x
- items관련하여 변경사항에 대해서 일일이 비교하거나 indexPath로 접근하지 않고 단순히 모든 배열을 dataSource에 던져주면 알아서 변경된 부분을 업데이트함
사용 방법
- 사용한 cocoa pod
- MVVM의 대표적인 형태인 ReactorKit 사용
pod 'ReactorKit'
pod 'RxSwift'
pod 'RxCocoa'
pod 'SnapKit'
pod 'Then'
- UITableViewDiffableDataSource를 사용하기 위해 별도의 클래스로 추가
- 별도의 클래스로 추가하는 이유: 테이블 뷰에서 편집 모드에서 셀 이동시키는 기능을 구현하려면 이곳에서 오버라이딩하여 구현해야 하기 때문에 이 기능을 위해 따로 빼놓기
- extension으로 update하는 로직 구현 (snapshot에서 모든 아이템을 지우고, 변경되거나 추가되거나 삭제된 아이템을 포함한 모든 아이템을 넣어주면 알아서 반영됨)
// MyDataSource.swift
final class MyDataSource: UITableViewDiffableDataSource<Int, MyItem> {
// 편집모드 > 셀 이동 처리
// override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
// <#code#>
// }
// override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
// <#code#>
// }
}
extension MyDataSource {
func update(items: [MyItem]) {
var snapshot = snapshot()
snapshot.deleteAllItems()
snapshot.appendSections([0])
snapshot.appendItems(items)
apply(snapshot)
}
}
- ViewController 쪽에서 MyDataSource 초기화
- 여기서 tableView를 넣어주기 때문에 별도의 tableView.dataSource = dataSource와 같은 코드는 필요 x
init() {
self.dataSource = MyDataSource(tableView: tableView, cellProvider: { tableView, indexPath, itemIdentifier in
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = itemIdentifier.value.description
return cell
})
super.init(nibName: nil, bundle: nil)
setupViews()
setupLayout()
}
- ViewController에서 reactor쪽으로 이벤트 전달
// MARK: Bind
func bind(reactor: MyReactor) {
// Action
Observable
.just(MyReactor.Action.load)
.bind(to: reactor.action)
.disposed(by: disposeBag)
appendButton.rx.tap
.throttle(.milliseconds(300), scheduler: MainScheduler.instance)
.map { Reactor.Action.append }
.bind(to: reactor.action)
.disposed(by: disposeBag)
removeButton.rx.tap
.throttle(.milliseconds(300), scheduler: MainScheduler.instance)
.map { Reactor.Action.remove }
.bind(to: reactor.action)
.disposed(by: disposeBag)
removeCenterButton.rx.tap
.throttle(.milliseconds(300), scheduler: MainScheduler.instance)
.map { Reactor.Action.removeCenter }
.bind(to: reactor.action)
.disposed(by: disposeBag)
}
- reactor쪽에서는 단순히 아이템을 받아서 업데이트 후 최종 items 배열을 변경
func mutate(action: Action) -> Observable<Mutation> {
switch action {
case .load:
return .just(.setItems([.init(value: 0), .init(value: 1), .init(value: 2), .init(value: 3), .init(value: 4)]))
case .append:
return .just(.appendItem)
case .remove:
return .just(.removeItem)
case .removeCenter:
return .just(.removeCenter)
}
}
func reduce(state: State, mutation: Mutation) -> State {
var state = state
switch mutation {
case let .setItems(array):
state.items = array
case .appendItem:
state.items.append(.init(value: state.items.count))
case .removeItem:
guard !state.items.isEmpty else { break }
state.items.removeLast()
case .removeCenter:
guard !state.items.isEmpty else { break }
let centerIndex = (state.items.count - 1) / 2
state.items.remove(at: centerIndex)
}
return state
}
- ViewController쪽에서는 items를 관찰하고 있다가 items 전체를 다시 update하면 자동으로 반영
// State
reactor.state.map(\.items)
.distinctUntilChanged()
.observe(on: MainScheduler.instance)
.bind(with: self) { ss, items in
ss.dataSource.update(items: items)
}
.disposed(by: self.disposeBag)
(완료)
HeaderView 사용 방법
- UITableView에서 사용 방법
- 프로퍼티인 sectionHeaderHeight값을 지정해주고 DataSource에서 viewForHeaderInSection 메소드에서 HeaderView를 반환하여 적용
- Diffable DataSource에서 사용 방법
- UITableViewDataSource를 사용하지 않고, Diffable DataSource에서도 viewForHeaderInSection 메소드가 없기 때문에 UITableView 프로퍼티에 대입해줘야함
let headerView = UIView()
view.tableHeaderView = headerView
headerView.frame.size.height = 100
정리
- Diffable DataSource 장점
- performBatchUpdates()는 크래시가 많이 나지만 Diffable DataSource를 사용하면 해결가능
- 변경된 사항에 대해서 item을 Diffable DataSource로 관리하면 매우 쉽게 처리가 가능 (변경사항에 대해서 일일이 비교 없이 모든 items를 Diffable DataSource에 던져주기만 해도 반영됨)
- Diffable DataSource 단점
- 편집모드에서 셀을 이동시키는 기능을 만들고 싶은 경우, DataSource 클래스를 따로 만들고 기존에 UITableViewDelegate에 있던 canMoveRowAt와moveRowAt을 이 DataSource에서 override해서 구현해야함
'iOS 응용 (swift)' 카테고리의 다른 글
Comments