Xcode/IOS

IOS) RxTableViewSectionedReloadDataSource를 실습해보기

후르륵짭짭 2021. 10. 24. 17:28
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
반응형