본문 바로가기
Xcode/IOS

IOS) RxTableViewSectionedReloadDataSource를 실습해보기

by 후르륵짭짭 2021. 10. 24.
728x90
반응형

안녕하세요. 짭짭이 입니다.

이번에는 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

 

Bring tables alive with RxDataSources. RxSwift part 1 - BytePace

Work with tables has never been easier

bytepace.com

https://nsios.tistory.com/32

 

[RxSwift] RxDataSources을 이용한 TableView구현하기

지금 까지 기본적인 입출력만 binding 하는것을 해봤어요 약간 더 어려운 Rx를 이용한 TableView를 구현해 볼거에요 앞의 내용을 하면서 Rx를 조금이라도 이해 했다면 그리 많이 어렵지 않을 거에여!!

nsios.tistory.com

https://ios-development.tistory.com/176

 

[RxSwift] 5. throttle, debounce

debounce 입력 -> 바로 입력 안되고 대기 -> 일정 시간 후 입력 주로 텍스트 필드 입력에 사용 코드 (쉬운 예시를 위해서 버튼 입력에 적용) btnDebounce.rx.tap.asDriver() .debounce(.seconds(3)) .drive(onNext..

ios-development.tistory.com

https://stackoverflow.com/questions/24031646/how-in-swift-specify-type-constraint-to-be-enum

 

How in Swift specify type constraint to be enum?

I want to specify a type constraint that the type should be a raw value enum: enum SomeEnum: Int { case One, Two, Three } class SomeProtocol> { // <- won't compile

stackoverflow.com

https://myseong.tistory.com/14

 

RxSwift UITableView (2) - More Section

RxSwift UITableView (2) - More Section RxSwift UITableView 에서 여러 section을 사용하는 방법에 대한 포스팅이다. 이전 포스팅에서는 한가지 Tyep의 Cell과 여러가지 Type의 Cell을 RxSwift로 구현했다...

myseong.tistory.com

https://kyungmosung.github.io/2020/02/13/rxswift-rxcocoa-tableview-collectionview/

 

[RxSwift] TableView, CollectionView in RxCocoa - Kyungmo's Blog

TableView

kyungmosung.github.io

 

https://rhammer.tistory.com/352

 

RxSwift - TableView, CollectionVIew를 사용해보자

Ch.18 Table and CollectionViews iOS앱에서 가장 많이 사용하는 UI는 UITableView, UICollectionVIew를 통해 데이터의 리스트를 표현하는 것이다. 보통은 delegate, dataSource의 콜백을 통해 데이터를 표현한다..

rhammer.tistory.com

 

728x90
반응형

댓글