안녕하세요. 짭짭이 입니다.
이번에는 RxDatasource의 RxTableViewSectionedReloadDataSource에 대해 알아 보도록 하겠습니다.
사내 프로젝트로 Rx를 많이 사용하게 되는데,,, 제가 rx지식이 많이 부족해서 연습이 쫌 많이 필요한 현실이라 ㅠㅠ
https://www.youtube.com/watch?v=Oke090IJDrI
< 그 시절의 노래를 들고 왔습니다. izi - 응급실 입니다.>
솔직히 UITableView를 사용하면서 크게 불편함을 느끼지 못 했습니다. (지금도 못 느끼고 있습니다. ㅎㅎㅎ)
또한 가능한면 UITableView가 기본적인 것이라서 사람들과 협업할 때는 실력의 편차 없이 서로 개발 할 수 있고 좋다고 생각합니다.
But!!!! TableCell이 점점 많아지는 상황일 때는 UITableView로 개발하는 것이 정신없게 만듭니다.
이럴 때 등장한 것이 RxDataSource라 생각이 듭니다.
그리고 IOS 개발을 하는데 Rx는 기본적으로 알아야하는 것이고 그럼 호기심으로라도 RxDatasource는 알아야한다 생각했습니다.
** 기본적인 세팅 **
- Import -
import UIKit
import RxSwift
import RxCocoa
import RxDataSources //TableView Section 사용하기 위해서는 필수적으로 Import
기본적으로 RxDatasource를 import 해줍니다.
- TableViewCell -
class TestViewCell : UITableViewCell {
let namelabel : UILabel = {
let l = UILabel()
l.translatesAutoresizingMaskIntoConstraints = false
return l
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setup()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setup(){
self.contentView.addSubview(namelabel)
NSLayoutConstraint.activate([
namelabel.topAnchor.constraint(equalTo: contentView.topAnchor),
namelabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
namelabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
namelabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
])
}
}
이렇게 TableViewCell을 기본적으로 생성 해줍니다.
** RxTableView에서 사용할 Model 생성 **
RxDataSource에서 가장 중요한것이구 핵심이라 할 수 있는게 Model이라 생각합니다.
1. 영화 구조체가 들어갈 것이고
2. Section에 해당 영화를 구분 할 수 있는 Enum이 있어야한다.
3. Section이 있어야한다.
- Movie라는 영화 구조체 생성 -
struct Movie {
let name : String
let price : Int
}
- RxDataSource에서 사용할 SectionModel -
일반적인 UITableView에서는 Array<Model> 이렇게 해주고 갯수만큼 보여주지만
RxDatasource는 SectionModel이라는 큰 상자 안에 필요한 아이템(Movie)를 넣습니다.
struct SectionModel<ItemGeneric , EnumGeneric : RawRepresentable> : SectionModelType where EnumGeneric.RawValue == Int{
var type : EnumGeneric
var items: [Item]
}
extension SectionModel {
typealias Item = ItemGeneric
//이건 사용안합니다.
init(original: SectionModel<ItemGeneric , EnumGeneric>, items: [Item]) {
self = original
self.items = items
}
}
이렇게 SectionModel이라는 RxDatasource에서 사용할 상자를 만들어 줍니다.
그리고 SectionModel은 RxDatasource가 사용할 수 있게 SectionModelType이라는 프로토콜을 준수 시킵니다.
준수시키면 Item 의 타입을 지정해주고 init()생성자를 저렇게 해줍니다.
여기서 init() 생성자는 사용하지 않습니다.
따라서 extension으로 나눠주면서 자동으로 생성해주는 (type : items :)를 사용할 수 있게 해줍니다.
$$ 참고 $$
SectionModel<ItemGeneric , EnumGeneric : RawRepresentable> : SectionModelType
where EnumGeneric.RawValue == Int
=> Enum을 제너릭으로 사용하기 위해서는 RawRepresentable을 준수해야합니다.
- Enum 생성 -
enum SectionType : Int {
case korea
case japan
case undefined
}
** RxDataSource TableView 생성 **
이제 TableView를 생성 해줄겁니다.
class RxTableViewSectioned : UIViewController {
lazy var mainTableView : UITableView = {
let tbv = UITableView()
tbv.translatesAutoresizingMaskIntoConstraints = false
tbv.backgroundColor = UIColor.yellow
tbv.register(TestViewCell.self, forCellReuseIdentifier: "TestViewCell")
return tbv
}()
lazy var buttonBar : UIButton = {
let barView = UIButton()
barView.translatesAutoresizingMaskIntoConstraints = false
barView.setTitle("Touch", for: .normal)
barView.setTitleColor(.blue, for: .normal)
return barView
}()
//DataSoure는 reloadData를 할 필요 없이 새로운 값이 Bind해서 들어오면 자동으로 Reload하게 해준다.(이게 개꿀인지는 고민)
private var dataSource : RxTableViewSectionedReloadDataSource< SectionModel<Movie, SectionType> >!
private var tableViewItems : BehaviorRelay<[SectionModel<Movie, SectionType>]> = BehaviorRelay(value: [])
private let disposBag = DisposeBag()
,,,,
}
여기서 젤 중요한 것은 아래 dataSource 입니다.
private var dataSource : RxTableViewSectionedReloadDataSource< SectionModel<Movie, SectionType> >!
이것이 RxTableView 입니다.
제너릭으로 되어 있고 안에 우리가 사용할 RxDataSource 모델인 SectionModel 을 넣어주고
이 상자 안에 필요한 아이템들을 넣어줄 겁니다.
** RxDataSource의 TableView 초기 설정 **
private func initDataSource(){
// SectionModelType의 Item에 들어가는 부분이 configureCell의 element에 들어가야한다.
let configureCell : (TableViewSectionedDataSource< SectionModel<Movie, SectionType> > , UITableView, IndexPath, Movie) -> UITableViewCell = { (datasource , tableview , indexpath , element) in
let cell : TestViewCell = tableview.dequeueReusableCell(withIdentifier: "TestViewCell", for: indexpath) as? TestViewCell ?? UITableViewCell() as! TestViewCell
cell.namelabel.text = "\(element.name) \(element.price)"
return cell
}
self.dataSource = .init(configureCell: configureCell)
self.dataSource.titleForHeaderInSection = { dataSource, index in
return "Header : \(self.dataSource.sectionModels[index].type)"
}
self.dataSource.titleForFooterInSection = { dataSource, index in
return "Footer : \(index)"
}
self.tableViewItems.asObservable()
.bind(to: mainTableView.rx.items(dataSource: self.dataSource))
.disposed(by: disposBag)
}
이게 기본적으로 많이 다른 부분인데,
// SectionModelType의 Item에 들어가는 부분이 configureCell의 element에 들어가야한다.
let configureCell : (TableViewSectionedDataSource< SectionModel<Movie, SectionType> > , UITableView, IndexPath, Movie) -> UITableViewCell = { (datasource , tableview , indexpath , element) in
let cell : TestViewCell = tableview.dequeueReusableCell(withIdentifier: "TestViewCell", for: indexpath) as? TestViewCell ?? UITableViewCell() as! TestViewCell
cell.namelabel.text = "\(element.name) \(element.price)"
return cell
}
configureCell은 UITableViewCell을 반환해주는 것으로 보아 Cell의 기본적인 설정을 해주는 부분 입니다.
여기 파라미터에 들어가는 것은 순서대로
1. RxDataSource TableView에서 사용할 모델(== SectionModel)
2. 자동으로 생성한 TableView
3. IndexPath
4. SectionModel내부에 있는 사용할 Item
이렇게 구성 되어 있습니다.
그리고 이렇게 생성한 configure를 datasource에 넣어줍니다.
self.dataSource = .init(configureCell: configureCell)
이게 끝 입니다.
그리고 정말 중요한 실수는
RxTableView는 viewDidAppear 이후에 해줘야 오토레이아웃 오류가 발생 안 합니다.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
initDataSource() // Rxswift는 ViewWDidAppear 이후에 해줘야 tableView의 autolayout에 대한 오류가 발생하지 않는다.
}
&& 참고 &&
RxDatasource의 특징은 Section에 좀더 친화적으로 되어 있습니다.
self.dataSource.titleForHeaderInSection = { dataSource, index in
return "Header : \(self.dataSource.sectionModels[index].type)"
}
self.dataSource.titleForFooterInSection = { dataSource, index in
return "Footer : \(index)"
}
이런 것을 기본적으로 제공 합니다.
self.dataSource.sectionModels[index].type
=> 각 Section의 내용에 접근하기 위해서는 datasource.sectionModel[인덱스].item
그리고 마지막에 아이템을 넣어줍니다.
self.tableViewItems.asObservable()
.bind(to: mainTableView.rx.items(dataSource: self.dataSource))
.disposed(by: disposBag)
여기서 주의할 것은 rxtableView에 아이템을 전달해주는 tableViewItems은 Array<SectionModel> 형태로
dataSource의 SectionModel과 동일한 형태야 합니다.
private var dataSource : RxTableViewSectionedReloadDataSource< SectionModel<Movie, SectionType> >!
private var tableViewItems : BehaviorRelay<[SectionModel<Movie, SectionType>]> = BehaviorRelay(value: [])
** RxDatasource에 Delegate 설정해주기 **
self.mainTableView.rx.setDelegate(self).disposed(by: disposBag)
이렇게 setDelegate 메소드르 통해서 UITableViewDelegate 설정 해줍니다.
그러면 아래 처럼 사용 할 수 있습니다.
extension RxTableViewSectioned : UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let cell = tableView.cellForRow(at: indexPath) as? TestViewCell else {return}
print("didSelectRowAt : \(cell.namelabel.text)")
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 100
}
}
** 특정 Section에 아이템 추가하기 **
//Button은 탭하면 Void가 내려온다.
//throttle은 탭을 누르면 바로 실행 되고 일정 시간 까지는 탭을 중복하여 눌러도 반응하지 않는다.
//다만 시간이내에 처음 누른 것과 마지막 누른 것 두번 발생한다.
//마지막에 나오는 것은 false로 방지 할 수 있다.
buttonBar.rx.tap.asDriver().throttle(.seconds(3), latest: false)//throttle(.seconds(3))
.drive(onNext:{ item in
var tempSectionModel = self.tableViewItems.value
if self.cnt == 0 {
tempSectionModel[SectionType.korea.rawValue].items.append(Movie(name: "Korea", price: 2000))
}
else if self.cnt == 1 {
tempSectionModel[SectionType.japan.rawValue].items.append(Movie(name: "Japan", price: 3000))
}
else{
tempSectionModel.append(SectionModel(type: SectionType.undefined, items: [Movie(name: "Undefined", price: 0)]) )
}
self.cnt += 1
self.tableViewItems.accept(tempSectionModel)
}).disposed(by: disposBag)
RxTableView의 단점은 특정 Section의 값을 변경해줄 때 쫌 복잡하다는게 단점 입니다.
self.tableViewItems.accept([])
을 넣어버리면 TableView의 데이터 들이 쏵~~ 사라집니다.
그리고 RxTableView 특성상 값을 전달 받으면 자동으로 reloadData를 해버립니다.
그래서 아래 처럼 한번 BehaviorRelay에 저장되어 있는 값을 가져온 다음에 추가해주고 값을 전달자에게 넘겨줘야합니다.
var tempSectionModel = self.tableViewItems.value
if self.cnt == 0 {
tempSectionModel[SectionType.korea.rawValue].items.append(Movie(name: "Korea", price: 2000))
}
else if self.cnt == 1 {
tempSectionModel[SectionType.japan.rawValue].items.append(Movie(name: "Japan", price: 3000))
}
else{
tempSectionModel.append(SectionModel(type: SectionType.undefined, items: [Movie(name: "Undefined", price: 0)]) )
}
self.cnt += 1
self.tableViewItems.accept(tempSectionModel)
&& 참고 &&
** throttle **
//throttle은 탭을 누르면 바로 실행 되고 일정 시간 까지는 탭을 중복하여 눌러도 반응하지 않는다.
//다만 시간이내에 처음 누른 것과 마지막 누른 것 두번 발생한다.
//마지막에 나오는 것은 false로 방지 할 수 있다.
buttonBar.rx.tap.asDriver().throttle(.seconds(3), latest: false)//throttle(.seconds(3))
.drive(onNext:{ item in
}).disposed(by: disposBag)
** debounce **
// debounce는 버튼을 누르고 일정시간 후에 실행 된다.(중복은 발생안함)
buttonBar.rx.tap.asDriver().debounce(.seconds(3))
.drive(onNext:{ item in
self.cnt += 1
print("Tap : " ,item , self.cnt)
}).disposed(by: disposBag)
** 전체 코드 **
import UIKit
import RxSwift
import RxCocoa
import RxDataSources //TableView Section 사용하기 위해서는 필수적으로 Import
class TestViewCell : UITableViewCell {
let namelabel : UILabel = {
let l = UILabel()
l.translatesAutoresizingMaskIntoConstraints = false
return l
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setup()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setup(){
self.contentView.addSubview(namelabel)
NSLayoutConstraint.activate([
namelabel.topAnchor.constraint(equalTo: contentView.topAnchor),
namelabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
namelabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
namelabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
])
}
}
struct Movie {
let name : String
let price : Int
}
struct SectionModel<ItemGeneric , EnumGeneric : RawRepresentable> : SectionModelType where EnumGeneric.RawValue == Int{
var type : EnumGeneric
var items: [Item]
}
extension SectionModel {
typealias Item = ItemGeneric
init(original: SectionModel<ItemGeneric , EnumGeneric>, items: [Item]) {
self = original
self.items = items
}
}
enum SectionType : Int {
case korea
case japan
case undefined
}
class RxTableViewSectioned : UIViewController {
lazy var mainTableView : UITableView = {
let tbv = UITableView()
tbv.translatesAutoresizingMaskIntoConstraints = false
tbv.backgroundColor = UIColor.yellow
tbv.register(TestViewCell.self, forCellReuseIdentifier: "TestViewCell")
return tbv
}()
lazy var buttonBar : UIButton = {
let barView = UIButton()
barView.translatesAutoresizingMaskIntoConstraints = false
barView.setTitle("Touch", for: .normal)
barView.setTitleColor(.blue, for: .normal)
return barView
}()
//DataSoure는 reloadData를 할 필요 없이 새로운 값이 Bind해서 들어오면 자동으로 Reload하게 해준다.(이게 개꿀인지는 고민)
private var dataSource : RxTableViewSectionedReloadDataSource< SectionModel<Movie, SectionType> >!
private var tableViewItems : BehaviorRelay<[SectionModel<Movie, SectionType>]> = BehaviorRelay(value: [])
private let disposBag = DisposeBag()
var cnt = 0
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(mainTableView)
NSLayoutConstraint.activate([
mainTableView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor),
mainTableView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
mainTableView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
mainTableView.heightAnchor.constraint(equalTo: self.view.heightAnchor, multiplier: 0.85)
])
view.addSubview(buttonBar)
NSLayoutConstraint.activate([
buttonBar.topAnchor.constraint(equalTo: self.mainTableView.bottomAnchor),
buttonBar.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
buttonBar.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
buttonBar.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor)
])
bindData()
#if false
// debounce는 버튼을 누르고 일정시간 후에 실행 된다.(중복은 발생안함)
buttonBar.rx.tap.asDriver().debounce(.seconds(3))
.drive(onNext:{ item in
self.cnt += 1
print("Tap : " ,item , self.cnt)
}).disposed(by: disposBag)
#endif
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
initDataSource() // Rxswift는 ViewWDidAppear 이후에 해줘야 tableView의 autolayout에 대한 오류가 발생하지 않는다.
}
}
extension RxTableViewSectioned {
private func bindData(){
initData()
self.mainTableView.rx.setDelegate(self).disposed(by: disposBag)
//Button은 탭하면 Void가 내려온다.
//throttle은 탭을 누르면 바로 실행 되고 일정 시간 까지는 탭을 중복하여 눌러도 반응하지 않는다.
//다만 시간이내에 처음 누른 것과 마지막 누른 것 두번 발생한다.
//마지막에 나오는 것은 false로 방지 할 수 있다.
buttonBar.rx.tap.asDriver().throttle(.seconds(3), latest: false)//throttle(.seconds(3))
.drive(onNext:{ item in
var tempSectionModel = self.tableViewItems.value
if self.cnt == 0 {
tempSectionModel[SectionType.korea.rawValue].items.append(Movie(name: "Korea", price: 2000))
}
else if self.cnt == 1 {
tempSectionModel[SectionType.japan.rawValue].items.append(Movie(name: "Japan", price: 3000))
}
else{
tempSectionModel.append(SectionModel(type: SectionType.undefined, items: [Movie(name: "Undefined", price: 0)]) )
}
self.cnt += 1
self.tableViewItems.accept(tempSectionModel)
}).disposed(by: disposBag)
}
private func initData(){
let korea : [Movie] = [
Movie(name: "전설의 왕국", price: 2000),
Movie(name: "킹덤", price: 1000),
Movie(name: "범죄의 전쟁", price: 500)
]
let japan : [Movie] = [
Movie(name: "바람의 검심", price: 2000),
Movie(name: "아리스 인 보더랜드", price: 1000)
]
let koreaSection = SectionModel(type: SectionType.korea, items: korea)
let japanSection = SectionModel<Movie , SectionType>(type: .japan, items: japan)
let items = [koreaSection , japanSection]
self.tableViewItems.accept(items)
}
private func initDataSource(){
// SectionModelType의 Item에 들어가는 부분이 configureCell의 element에 들어가야한다.
let configureCell : (TableViewSectionedDataSource< SectionModel<Movie, SectionType> > , UITableView, IndexPath, Movie) -> UITableViewCell = { (datasource , tableview , indexpath , element) in
let cell : TestViewCell = tableview.dequeueReusableCell(withIdentifier: "TestViewCell", for: indexpath) as? TestViewCell ?? UITableViewCell() as! TestViewCell
cell.namelabel.text = "\(element.name) \(element.price)"
return cell
}
self.dataSource = .init(configureCell: configureCell)
self.dataSource.titleForHeaderInSection = { dataSource, index in
return "Header : \(self.dataSource.sectionModels[index].type)"
}
self.dataSource.titleForFooterInSection = { dataSource, index in
return "Footer : \(index)"
}
self.tableViewItems.asObservable()
.bind(to: mainTableView.rx.items(dataSource: self.dataSource))
.disposed(by: disposBag)
}
private func getTableViewCell(){}
}
extension RxTableViewSectioned : UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let cell = tableView.cellForRow(at: indexPath) as? TestViewCell else {return}
print("didSelectRowAt : \(cell.namelabel.text)")
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 100
}
}
참고 사이트 :
http://bytepace.com/blog/rxswift
https://ios-development.tistory.com/176
https://stackoverflow.com/questions/24031646/how-in-swift-specify-type-constraint-to-be-enum
https://myseong.tistory.com/14
https://kyungmosung.github.io/2020/02/13/rxswift-rxcocoa-tableview-collectionview/
https://rhammer.tistory.com/352
'Xcode > IOS' 카테고리의 다른 글
IOS)Moya 간단 사용 정리하기 (0) | 2022.06.26 |
---|---|
SwiftUI) LazyGride 대한 경험 정리 (0) | 2022.06.11 |
IOS)Rx의 Publish와 Subscribe를 MVVM으로 구현해보기 (0) | 2021.09.22 |
IOS) Literal에 대해서 알아보자! (0) | 2021.01.31 |
IOS) NSFetchedResultsController을 이용하자! (0) | 2021.01.12 |
댓글