apple/UIKit & ReactiveX
ReactorKit + RxDataSources #1(SectionModelType)
lgvv
2022. 10. 5. 12:49
ReactorKit + RxDataSources #1(SectionModelType)
개발환경
ReactorKit 3.2.0
RxDataSources 5.0.2
SnapKit 5.6.0
Xcode 14.0
RxDataSource는 이전에도 많이 사용해봄.
DiffableDataSource를 이해하고 있다면 사용하기 더 편함.
이 포스팅은 RxDataSources 사용하는 방법의 순서도(?)를 최대한 기록해 두려고 함.
특징:
1. 섹션마다 다른 셀을 적용
2. tableView를 활용하여 동적 높이를 구현함.
3. ReactorKit + RxDataSource
4. cellReactor에 initialState의 값을 Model로 초기화
🌿 결과물 UI 🌿
✅ 전체 코드 ✅
Step 1. 모델을 정의하기
// ✅ 0. 사용할 모델읠 정의
class GWRxDataSourceModel {
var id = UUID().uuidString
var content: String?
var imageUrl: String?
convenience init(content: String?, imageUrl: String? = nil) {
self.init()
self.content = content
self.imageUrl = imageUrl
}
}
Step 2. Section 정의하기
import RxDataSources
// ✅ 1. Item을 정의합니다. -> 단일 섹션에 들어갈 수 있는 셀들을 정의
enum TableViewCellSectionItem {
// ✅ 1-1. Reactor를 셀에 붙여줄 것이므로 이렇게 설계합니다.
case grayTextCell(GWGrayTextCellReactor)
case toggleTextCell(GWToggleTextCellReactor)
case imageCell(GWGWImageCellReactor)
}
// ✅ 2. Section을 정의합니다. -> 섹션!
enum TableViewCellSection {
case grayTextSection([TableViewCellSectionItem])
case toggleTextSection([TableViewCellSectionItem])
case imageSection([TableViewCellSectionItem])
}
// ✅ 3. 섹션을 RxDataSource의 SectionModelType을 상속받아서 구현
// - SectionModelType
// - AnimatableSectionModelType
// 다르게 사용할 수 있음
extension TableViewCellSection: SectionModelType {
typealias Item = TableViewCellSectionItem
// ✅ 3-1. SectionModelType에 있는 아이템을 정의 각 아이템이 어떤 섹션으로 연결되는지를 결정.
var items: [Item] {
switch self {
case .grayTextSection(let items):
return items
case .toggleTextSection(let items):
return items
case .imageSection(let items):
return items
}
}
// ✅ 3-2. init을 통해서 각 아이템에 맞도록 매핑
init(original: TableViewCellSection, items: [TableViewCellSectionItem]) {
switch original {
case .grayTextSection:
self = .grayTextSection(items)
case .toggleTextSection:
self = .toggleTextSection(items)
case .imageSection:
self = .imageSection(items)
}
/*
✅ 위의 코드와 같은 문법
var original = original
switch original {
case .grayTextSection(let items):
original = .grayTextSection(items)
case .toggleTextSection(let items):
original = .toggleTextSection(items)
case .imageSection(let items):
original = .imageSection(items)
}
self.init(original: original, items: items)
*/
}
}
Step 3. ViewReactor 정의하기
NOTE: - 만약 네트워크 통신 혹은 로컬에서 데이터를 가져온다면 confugurationSections()에서 service나 useCase 등을 사용하여 데이터를 가져와 줍니다.
import Foundation
import ReactorKit
import RxDataSources
class GWViewReactor: Reactor {
// ✅ 4. CellType정의
enum CellType {
case grayTextCell(String, String)
case toggleTextCell(String)
case imageCell(String)
}
// MARK: - Property
let initialState: State
// MARK: - Constants
enum Action {
case cellSelected(IndexPath)
}
enum Mutation {
case setSelectedIndexPath(IndexPath?)
}
struct State {
var selectedIndexPath: IndexPath?
var sections: [TableViewCellSection] // ✅ 5. sections 정의
}
init() {
self.initialState = State(
sections: GWViewReactor.confugurationSections() // ✅ 6. section을 reaction의 confugurationSections 메소드를 통해 값을 주입
)
}
// MARK: - func
func mutate(action: Action) -> Observable<Mutation> {
switch action {
case .cellSelected(let indexPath):
return Observable.concat([
Observable.just(Mutation.setSelectedIndexPath(indexPath)),
Observable.just(Mutation.setSelectedIndexPath(nil))
])
}
}
func reduce(state: State, mutation: Mutation) -> State {
var newState = state
switch mutation {
case .setSelectedIndexPath(let indexPath):
newState.selectedIndexPath = indexPath
}
return newState
}
// ✅ 7. section을 reaction의 confugurationSections 메소드를 통해 값을 주입
static func confugurationSections() -> [TableViewCellSection] {
// ✅ 7-1. 셀 세팅
let defaultCell = TableViewCellSectionItem.grayTextCell(GWGrayTextCellReactor(initialState: GWRxDataSourceModel(content: "여기는 textCell입니다. 여기는 텍스트가 들어가고 검정색 글자로 들어갑니다.", imageUrl: nil)))
let defaultCell2 = TableViewCellSectionItem.grayTextCell(GWGrayTextCellReactor(initialState: GWRxDataSourceModel(content: "여기도 textCell입니다. 여기도 마찬가지지만 위랑 차이점은 height을 동적으로 구성할 수 있다는 것을 보여들고자 했습니다. 그리니까 넘 신경쓰지 않아주셔도 됩니다. 그냥 길이만 길어요. 지금부터는 아무말이나 적어서 길이를 늘려야 하는데요. 한글이 조금 더 많이 늘릴 수 있어서 한글로 적고 있습니다. 지금 저는 신공학관 로비에 있는데, 우산이 없습니다. 바깥에는 비가 우르르 쾅쾅오려고 합니다. 슬프지만 어쩔 수가 없습니다. 비를 맞으면서 집에 가야겠네요.")))
// ✅ 7-1. 섹션 세팅
let defaultsection = TableViewCellSection.grayTextSection([defaultCell, defaultCell2])
let toggleCell1 = TableViewCellSectionItem.toggleTextCell(GWToggleTextCellReactor(initialState: GWRxDataSourceModel(content: "안녕하세요. 여기가 toggleTextCell입니다.")))
let toggleCell2 = TableViewCellSectionItem.toggleTextCell(GWToggleTextCellReactor(initialState: GWRxDataSourceModel(content: "toggleTextCell입니다. 근데 길이가 길어지면 어떻게 될까요? 이렇게 처리하고자 합니다. 이건 뭐 UI라 변경할 수 있습니다. 간단하니까 너무 뭐라고 하지 마세요 다들 화이팅 입니다.")))
let switchSection = TableViewCellSection.toggleTextSection([toggleCell1, toggleCell2])
let defaultCell3 = TableViewCellSectionItem.grayTextCell(GWGrayTextCellReactor(initialState: GWRxDataSourceModel(content: "여기는 textCell입니다. 여기는 텍스트가 들어가고 검정색 글자로 들어갑니다. ")))
let defaultCell4 = TableViewCellSectionItem.grayTextCell(GWGrayTextCellReactor(initialState: GWRxDataSourceModel(content: "여기도 textCell입니다. 여기도 마찬가지지만 위랑 차이점은 height을 동적으로 구성할 수 있다는 것을 보여들고자 했습니다. 그리니까 넘 신경쓰지 않아주셔도 됩니다. 그냥 길이만 길어요. 지금부터는 아무말이나 적어서 길이를 늘려야 하는데요. 한글이 조금 더 많이 늘릴 수 있어서 한글로 적고 있습니다. 지금 저는 신공학관 로비에 있는데, 우산이 없습니다. 바깥에는 비가 우르르 쾅쾅오려고 합니다. 슬프지만 어쩔 수가 없습니다. 비를 맞으면서 집에 가야겠네요.")))
let defaultsection2 = TableViewCellSection.grayTextSection([defaultCell3, defaultCell4])
let toggleCell3 = TableViewCellSectionItem.toggleTextCell(GWToggleTextCellReactor(initialState: GWRxDataSourceModel(content: "안녕하세요. 여기가 toggleTextCell입니다.")))
let switchSection2 = TableViewCellSection.toggleTextSection([toggleCell3])
let imageCell = TableViewCellSectionItem.imageCell(GWGWImageCellReactor(initialState: GWRxDataSourceModel(content: nil, imageUrl: "heartIcon")))
let imageCell2 = TableViewCellSectionItem.imageCell(GWGWImageCellReactor(initialState: GWRxDataSourceModel(content: nil, imageUrl: "mapIcon")))
let imageSection = TableViewCellSection.imageSection([imageCell, imageCell2])
// ✅ 7-2. 섹션값들 반환
return [defaultsection, switchSection, defaultsection2, switchSection2, imageSection]
/*
✅ NOTE: - 만약 셀마다 서로 다른 모델을 사용할 경우에는 CellReactor에서 모델을 바꿔주면 된다!
*/
}
}
Step 4. ViewController 정의하기
import UIKit
import ReactorKit
import RxDataSources
import RxCocoa
import RxSwift
// ✅ 8. RxDataSourceDataSource정의
typealias ManageMentDataSource = RxTableViewSectionedReloadDataSource<TableViewCellSection>
//typealias ManageMentDataSource = RxTableViewSectionedAnimatedDataSource<TableViewCellSection> // 🚨 Error: - Diffable처럼 사용하기 위해서는 이거 학습하기
class GWViewController: UIViewController, View {
typealias Reactor = GWViewReactor
// MARK: - Properties
var disposeBag = DisposeBag()
// ✅ 9. dataSource값을 세팅
let dataSource: ManageMentDataSource = ManageMentDataSource(configureCell: { _, tableView, indexPath, items -> UITableViewCell in
// ✅ 9-1. 아이템에 따라서 값을 세팅
switch items {
case .grayTextCell(let reactor):
guard let cell = tableView.dequeueReusableCell(
withIdentifier: GWGrayTextCell.identifier,
for: indexPath
) as? GWGrayTextCell else { return UITableViewCell() }
cell.reactor = reactor
return cell
case .toggleTextCell(let reactor):
guard let cell = tableView.dequeueReusableCell(
withIdentifier: GWToggleTextCell.identifier,
for: indexPath
) as? GWToggleTextCell else { return UITableViewCell() }
cell.reactor = reactor
return cell
case .imageCell(let reactor):
guard let cell = tableView.dequeueReusableCell(
withIdentifier: GWImageCell.identifier,
for: indexPath
) as? GWImageCell else { return UITableViewCell() }
cell.reactor = reactor
return cell
}
})
// MARK: - Binding
func bind(reactor: GWViewReactor) {
// ✅ 10. 만약 tableView의 delegate를 설정한다면 다음과 같이 줄 수 있음.
// tableView.rx.setDelegate(self).disposed(by: disposeBag)
// MARK: - Action
tableView.rx.itemSelected
.map { Reactor.Action.cellSelected($0) }
.bind(to: reactor.action)
.disposed(by: disposeBag)
// MARK: - State
reactor.state.map { $0.selectedIndexPath }
.compactMap { $0 } // nil 제거
.withUnretained(self)
.bind { owner, indexPath in
owner.tableView.deselectRow(at: indexPath, animated: true)
}
.disposed(by: disposeBag)
// ✅ 11. 핵심! -> section을 바인딩하여 dataSource를 주면 된다.
reactor.state.map { $0.sections }
.asObservable()
.bind(to: self.tableView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
}
// MARK: - Initialize
init(reactor: Reactor) {
super.init(nibName: nil, bundle: nil)
self.reactor = reactor
setUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setUI() {
view.addSubview(tableView)
tableView.snp.makeConstraints {
$0.edges.equalToSuperview()
}
}
// MARK: - UIComponents
// ✅ 12. 셀을 다 등록해주면 끄읏!
var tableView: UITableView = {
var tv = UITableView(frame: .zero, style: .plain)
tv.register(GWGrayTextCell.self, forCellReuseIdentifier: GWGrayTextCell.identifier)
tv.register(GWToggleTextCell.self, forCellReuseIdentifier: GWToggleTextCell.identifier)
tv.register(GWImageCell.self, forCellReuseIdentifier: GWImageCell.identifier)
tv.rowHeight = UITableView.automaticDimension
tv.estimatedRowHeight = 56
return tv
}()
}
//extension GWViewController: UITableViewDelegate {
//// func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
//// return 56
//// }
//}
Step 5. Cell 및 CellReactor 정의하기
import UIKit
import ReactorKit
class GWToggleTextCell: UITableViewCell, View {
static let identifier = String(describing: self)
typealias Reactor = GWToggleTextCellReactor
var disposeBag = DisposeBag()
// MARK: - Binding
func bind(reactor: Reactor) {
// ✅ 13. reactor에서 content를 가져와서 Cell을 그리기
// cellReactor의 initialState의 값이 Model로 정의하는 스킬
reactor.state.compactMap { $0.content }
.withUnretained(self)
.bind { owner, text in owner.contentLabel.text = text }
.disposed(by: disposeBag)
}
// MARK: - Initialize
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - Binding
private func setUI() {
[toggleView, contentLabel].forEach { contentView.addSubview($0) }
toggleView.backgroundColor = .black
toggleView.snp.makeConstraints {
$0.top.equalToSuperview().inset(30)
$0.leading.equalToSuperview().inset(30)
$0.width.height.equalTo(10)
}
toggleView.layer.cornerRadius = 5
contentLabel.textColor = .black
contentLabel.numberOfLines = 0
contentLabel.lineBreakMode = .byCharWrapping
contentLabel.font = UIFont.systemFont(ofSize: 18, weight: .bold)
contentLabel.snp.makeConstraints {
$0.top.equalTo(toggleView.snp.top)
$0.leading.equalTo(toggleView.snp.trailing).offset(10)
$0.bottom.equalToSuperview().inset(10)
$0.trailing.equalToSuperview().inset(10)
}
}
// MARK: - UIComponents
let toggleView = UIView()
let contentLabel = UILabel()
}
// ✅ 14. cellReactor를 정의
class GWToggleTextCellReactor: Reactor {
typealias Action = NoAction
let initialState: GWRxDataSourceModel // ✅ 15. (핵심) 초기 스테이트를 모델로 초기화
init(initialState: GWRxDataSourceModel) {
self.initialState = initialState
}
}
다음 포스팅에는 DiffableDataSource처럼 사용할 수 있는 RxDataSources의 RxTableViewSectionedAnimatedDataSource을 알아보도록 하자.
다음글 주소
2022.10.05 - [iOS/🦕 RxSwift] - ReactorKit + RxDataSources #2(RxTableViewSectionedAnimatedDataSource)