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 | 31 |
Tags
- UICollectionView
- map
- collectionview
- 리펙토링
- combine
- RxCocoa
- uiscrollview
- HIG
- 리펙터링
- 스위프트
- ios
- uitableview
- rxswift
- clean architecture
- SWIFT
- 클린 코드
- Refactoring
- MVVM
- UITextView
- Observable
- tableView
- 애니메이션
- swiftUI
- Xcode
- Clean Code
- swift documentation
- Human interface guide
- ribs
- Protocol
- 리팩토링
Archives
- Today
- Total
김종권의 iOS 앱 개발 알아가기
[iOS - swift] 2. ReactorKit 샘플 앱 - RxDataSources을 이용한 Section, Item 모델 구현 패턴 (with 동적 사이즈 셀) 본문
iOS framework
[iOS - swift] 2. ReactorKit 샘플 앱 - RxDataSources을 이용한 Section, Item 모델 구현 패턴 (with 동적 사이즈 셀)
jake-kim 2021. 12. 16. 23:531. ReactorKit 샘플 앱 - RxDataSources 사용 방법
2. ReactorKit 샘플 앱 - RxDataSources을 이용한 Section, Item 모델 구현 패턴 (with 동적 사이즈 셀)
기초
목차
1. 단일 Section 모델
2. 다중 Section 모델
3. 다중 Section, 다중 Item 모델
구현 핵심
- SectionModel 정의 및 활용 방법
- Section이 하나인 경우에는 SectionModel<Int, MyItem>으로, Section에는 Int형을 사용
(가장 간단한 방법은 tableView.rx.items 방법 포스팅 글 참고)
// Section이 하나인 경우 import RxDataSources struct SingleSection { typealias MessageSectionModel = SectionModel<Int, MessageItem> enum MessageItem: Equatable { case firstItem(Message) } }
- Section이 여러개인 경우, 내부에 Section을 별도로 만들어서 그 타입을 Int대신 사용 SectionModel<MySection, MyItem>
// Section이 여러개인 경우 import RxDataSources struct MultiSection { typealias MessageSectionModel = SectionModel<MessageSection, MessageItem> enum MessageSection: Equatable { case main case sub } enum MessageItem: Equatable { case firstItem(Message) } }
1. 단일 Section 모델
- Section이 한개인 경우 모델
import RxDataSources struct SingleSection { typealias MessageSectionModel = SectionModel<Int, MessageItem> enum MessageItem: Equatable { case firstItem(Message) } }
- ViewController쪽에서 dataSource 정의
- dataSource의 타입은 배열이 아니라 Model하나로만 정의해놓고, bind할때 Array로 감싸서 dataSource로 방출
func bind(reactor: SingleViewReactor) { // Action ... // State let dataSource = RxTableViewSectionedReloadDataSource<SingleSection.MessageSectionModel> { dataSource, tableView, indexPath, item in switch item { case .firstItem(let message): let cell = tableView.dequeueReusableCell(for: indexPath) as MessageTableViewCell cell.setMessage(message) return cell } } reactor.state.map(\.messageSection) .distinctUntilChanged() .map(Array.init(with:)) // <- extension으로 Array 초기화 시 차원을 하나 늘려주는 코드추가 .bind(to: self.myTableView.rx.items(dataSource: dataSource)) .disposed(by: self.disposeBag) }
- dataSource의 타입은 배열이 아니라 Model하나로만 정의해놓고, bind할때 Array로 감싸서 dataSource로 방출
- ViewReactor에서 dataSource 모델을 정의하고, MessageSectionModel타입은 SectionModel<Int, Items> 타입
- SectionModel의 초기화는 init(model:items:)이고, model자리에 Section 타입이 들어가므로 0으로 할당
final class SingleViewReactor: Reactor { enum Action { case viewDidLoad } enum Mutation { case updateDataSource } struct State { var messageSection = SingleSection.MessageSectionModel( model: 0, items: [] ) } ... }
- 데이터가 변경될때도 model인자에 0으로 초기화
func reduce(state: State, mutation: Mutation) -> State { var state = state switch mutation { case .updateDataSource: let messages = getMessageMock() let myItems = messages.map(SingleSection.MessageItem.firstItem) let mySectionModel = SingleSection.MessageSectionModel(model: 0, items: myItems) // <- 여기, section에는 0으로 초기화 state.messageSection = mySectionModel } return state }
- SectionModel의 초기화는 init(model:items:)이고, model자리에 Section 타입이 들어가므로 0으로 할당
2. 다중 Section 모델
- Section이 여러개인 경우 모델
- SectionModel에서 Section 타입 자리에 이번엔 Int대신 Section타입으로 선언 (Int인 경우 단일 section으로 사용)
import RxDataSources struct MultiSection { typealias MessageSectionModel = SectionModel<MessageSection, MessageItem> enum MessageSection: Equatable { case main case sub } enum MessageItem: Equatable { case firstItem(Message) } }
- SectionModel에서 Section 타입 자리에 이번엔 Int대신 Section타입으로 선언 (Int인 경우 단일 section으로 사용)
- ViewController쪽에서 dataSource 정의
- dataSource의 타입은 배열이 아니라 Model하나로만 정의해놓고, bind할때 Array로 감싸서 dataSource로 방출
func bind(reactor: MultiViewReactor) { // Action ... // State let dataSource = RxTableViewSectionedReloadDataSource<MultiSection.MessageSectionModel> { dataSource, tableView, indexPath, item in switch item { case .firstItem(let message): let cell = tableView.dequeueReusableCell(for: indexPath) as MessageTableViewCell cell.setMessage(message) return cell } } reactor.state.map(\.messageSection) .distinctUntilChanged() .map(Array.init(with:)) // <- extension으로 Array 초기화 시 차원을 하나 늘려주는 코드추가 .bind(to: self.myTableView.rx.items(dataSource: dataSource)) .disposed(by: self.disposeBag) }
- dataSource의 타입은 배열이 아니라 Model하나로만 정의해놓고, bind할때 Array로 감싸서 dataSource로 방출
- ViewReactor에서 dataSource 모델을 정의하고, MessageSectionModel타입은 SectionModel<Section, Items> 타입
- SectionModel의 초기화는 init(model:items:)이고, model자리에 정의한 sectino타입 주입
(단일인 경우에는 model자리에 Section 타입이 들어가므로 0으로 할당)
final class MultiViewReactor: Reactor { enum Action { case viewDidLoad } enum Mutation { case updateDataSource } struct State { var messageSection = MultiSection.MessageSectionModel( model: .main, items: [] ) } ... }
- SectionModel의 초기화는 init(model:items:)이고, model자리에 정의한 sectino타입 주입
- 데이터가 변경될때 model인자에 정의한 타입으로 초기화
func reduce(state: State, mutation: Mutation) -> State { var state = state switch mutation { case .updateDataSource: let messages = getMessageMock() let myItems = messages.map(SingleSection.MessageItem.firstItem) let mySectionModel = MultiSection.MessageSectionModel(model: .main, items: myItems) // <- 여기, section에는 0으로 초기화 state.messageSection = mySectionModel } return state }
3. 다중 Section, 다중 Item
- 핵심 키워드
- tableView.rowHeight = UITableView.automaticDimension
- Reactor.State에 Section 모두 정의
- ViewController에서 state 바인딩 시, reactor.state.map { [$0.messageSection, $0.imageSection] }
- Section이 여러개인 경우 모델
- Section이 여러개, Item이 여러개
import RxDataSources struct ComplexSection { typealias Model = SectionModel<MySection, MyItem> enum MySection: Equatable { case message case image } enum MyItem: Equatable { case message(Message) case photo(UIImage?) } }
- Section이 여러개, Item이 여러개
- Reactor에서 State를 Section 별로 선언
final class ComplexViewReactor: Reactor { enum Action { case viewDidLoad } enum Mutation { case setPhotos case setMessages } struct State { var messageSection = ComplexSection.Model( model: .message, items: [] ) var imageSection = ComplexSection.Model( model: .image, items: [] ) } ... }
- mutate(action:)에서 concat으로 방출
// ComplexViewReactor.swift func mutate(action: Action) -> Observable<Mutation> { switch action { case .viewDidLoad: return .concat( [Observable<Mutation>.just(.setMessages), Observable<Mutation>.just(.setPhotos) ] ) } } func reduce(state: State, mutation: Mutation) -> State { var state = state switch mutation { case .setMessages: let messages = getMessageMock() let myItems = messages.map(ComplexSection.MyItem.message) let mySectionModel = ComplexSection.Model(model: .message, items: myItems) state.messageSection = mySectionModel case .setPhotos: let photo = UIImage(named: "snow") let myItems = ComplexSection.MyItem.photo(photo) let mySectionModel = ComplexSection.Model(model: .image, items: [myItems]) state.imageSection = mySectionModel } return state }
- ViewController에서 tableView.rowHeight = UITableVIew.automaticDimension
- automaticDimension 사용 주의: Cell의 Autolayout을 완벽하게 하지 않으면 동적으로 size가 유지되지 않으므로 주의
// ComplexViewController.swift private let myTableView = UITableView().then { $0.register(cellType: MessageTableViewCell.self) $0.register(cellType: PhotoTableViewCell.self) $0.rowHeight = UITableView.automaticDimension // <- 이곳 }
- automaticDimension 사용 주의: Cell의 Autolayout을 완벽하게 하지 않으면 동적으로 size가 유지되지 않으므로 주의
- bind(reactor:)에서 dataSource 정의
// ComplexViewController.swift func bind(reactor: ComplexViewReactor) { // Action ... // State let dataSource = RxTableViewSectionedReloadDataSource<ComplexSection.Model> { dataSource, tableView, indexPath, item in Self.configureCollectionViewCell( tableView: tableView, indexPath: indexPath, item: item ) } dataSource.titleForHeaderInSection = { dataSource, index in let section = dataSource.sectionModels[index].model switch section { case .message: return "Message Section" case .image: return "Image Section" } } }
- 바인딩
- section에 관한 바인딩을 한꺼번에 할 것 [$0.messageSection, $0.imageSection]
// ComplexViewController.swift func bind(reactor: ComplexViewReactor) { // Action ... // State ... reactor.state.map { [$0.messageSection, $0.imageSection] } .distinctUntilChanged() .bind(to: self.myTableView.rx.items(dataSource: dataSource)) .disposed(by: disposeBag) }
- section에 관한 바인딩을 한꺼번에 할 것 [$0.messageSection, $0.imageSection]
- 바인딩 시 주의 - 바인딩을 따로하게되면 dataSource를 중복으로 바인딩되는걸로 인식되므로 compile error 발생
// 이렇게 따로 바인딩하지 않는것을 주의 (compile error) reactor.state.map(\.messageSection) .distinctUntilChanged() .map(Array.init(with:)) .bind(to: self.myTableView.rx.items(dataSource: dataSource)) .disposed(by: self.disposeBag) reactor.state.map(\.imageSection) .distinctUntilChanged() .map(Array.init(with:)) .bind(to: self.myTableView.rx.items(dataSource: dataSource)) .disposed(by: self.disposeBag)
* 전체 소스 코드: https://github.com/JK0369/ExRxDataSourcePattern
'iOS framework' 카테고리의 다른 글
Comments