apple/UIKit & ReactiveX
ReactorKit + RxDataSources #2(RxTableViewSectionedAnimatedDataSource)
lgvv
2022. 10. 5. 23:51
ReactorKit + RxDataSources #2
(RxTableViewSectionedAnimatedDataSource)
개발환경
ReactorKit 3.2.0
RxDataSources 5.0.2
SnapKit 5.6.0
Xcode 14.0
1편을 기반으로 하고 있습니다.
TableView를 DiffableDataSource처럼 애니메이션이 적용되도록 구현하기 위해
RxTableViewSectionedAnimatedDataSource으로 변경하여 구현함.
1편 주석은 ✅ (0~14번)
2편의 주석은 🍋 (1~5번)
🌿 결과물 UI 🌿
- RxTableViewSectionedAnimatedDataSource 사용시 애니메이션이 적용되는지 확인하기 위해 itemSelect시 첫번째 섹션 삭제
- RxTableViewSectionedReloadDataSource의 경우에는 reloadData()로 애니메이션 작동 안함.
Step 1. 모델을 정의하기
import UIKit
import RealmSwift
import Differentiator // 🍋 0. Differentinator를 import
// Differentinator는 IdentifiableType 등을 정의해 두었음.
// ✅ 0. 사용할 모델읠 정의
class GWRxDataSourceModel: IdentifiableType {
// 🍋 1. DiffableDataSource에서 그랬듯이 Hashable한 id값을 갖고 변화를 만든다.
let identity: String = 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 UIKit
import RxDataSources
import Differentiator
// ✅ 1. Item을 정의합니다. -> 단일 섹션에 들어갈 수 있는 셀들을 정의
// 🍋 2. Equatable를 상속받아야 함. -> 그 이유는 AnimatableSectionModelType가 요구함.
enum TableViewCellSectionItem: Equatable, IdentifiableType {
var identity: some Hashable { UUID().uuidString }
// ✅ 1-1. Reactor를 셀에 붙여줄 것이므로 이렇게 설계합니다.
case grayTextCell(GWGrayTextCellReactor)
case toggleTextCell(GWToggleTextCellReactor)
case imageCell(GWGWImageCellReactor)
static func == (lhs: TableViewCellSectionItem, rhs: TableViewCellSectionItem) -> Bool {
lhs.identity == rhs.identity
}
}
// ✅ 2. Section을 정의합니다. -> 섹션!
enum TableViewCellSection {
case grayTextSection([TableViewCellSectionItem])
case toggleTextSection([TableViewCellSectionItem])
case imageSection([TableViewCellSectionItem])
}
// ✅ 3. 섹션을 RxDataSource의 SectionModelType을 상속받아서 구현
// - SectionModelType
// - AnimatableSectionModelType
// 다르게 사용할 수 있음
// 🍋 3. AnimatableSectionModelType을 상속
extension TableViewCellSection: AnimatableSectionModelType {
// 🍋 4. identity를 선언 Hashable!
var identity: String { UUID().uuidString }
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 정의하기
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))
])
}
}
func reduce(state: State, mutation: Mutation) -> State {
var newState = state
switch mutation {
case .setSelectedIndexPath(let indexPath):
newState.selectedIndexPath = indexPath
// 🍋 6. 그냥 애니메이션을 보기 위한 코드
var sections = GWViewReactor.confugurationSections()
sections.removeFirst()
newState.sections = sections
}
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]
return [defaultsection, switchSection, defaultsection2, switchSection2]
/*
✅ NOTE: - 만약 셀마다 서로 다른 모델을 사용할 경우에는 CellReactor에서 모델을 바꿔주면 된다!
*/
}
}
Step 4. ViewController 정의하기
// ✅ 8. RxDataSourceDataSource정의
//typealias ManageMentDataSource = RxTableViewSectionedReloadDataSource<TableViewCellSection>
// 🍋 5. RxTableViewSectionedAnimatedDataSource로 정의 -> 이후에는 똑같음
typealias ManageMentDataSource = RxTableViewSectionedAnimatedDataSource<TableViewCellSection>
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
}
}
다음에는 셀안에 컬렉션 뷰나 테이블 뷰를 넣어보자!