ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • rxSwift + mvvm 구조로 콜렉션뷰 설계하기 (2)
    iOS/mvvm + RxSwift 스터디 2018. 12. 4. 21:59

    https://sesang06.tistory.com/98


    전에 포스트했던 콜렉션뷰는 섹션이 하나도 없었습니다.

    RxDatasource을 임포트해 

    콜렉션뷰에 섹션을 추가해보겠습니다.


    protocol CodeBlock : Equatable {

        var codeBlockName : String { get }

        var codeBlockId : Int { get }

    }

    extension CodeBlock {

        static func == (lhs: Self, rhs: Self) -> Bool {

            return lhs.codeBlockName == rhs.codeBlockName

        }

    }


    protocol CodeSentence {

        associatedtype T

        var selectedCodeBlocks : Variable<[T]> { get set }

        var candidateCodeBlocks : Variable<[T]> { get }

    //    var answerCodeBlocks : [T] { get }

        

    }

    //

    //extension CodeSentence {

    //    func isRightSentence() -> Bool {

    //        return selectedCodeBlocks.value == answerCodeBlocks

    //    }

    //

    //}


    class EnglishCodeBlock : CodeBlock {

        var codeBlockName: String

        

        var codeBlockId: Int

        init(name: String){

            self.codeBlockName = name

            self.codeBlockId = 0

        }

    }



    설계가 약간 바뀌어서 맞지 않은 부분을 살짝 수정했습니다.



    enum EnglishCodeSentenceCollectionViewCellItem {

        case selected(cellViewModel : EnglishCodeBlock)

        case candidate(cellViewModel : EnglishCodeBlock, isSelected : Bool)

    }

    enum EnglishCodeSentenceCollectionViewSectionModel {

        case selectedSection(items : [EnglishCodeSentenceCollectionViewCellItem])

        case candidateSection(items : [EnglishCodeSentenceCollectionViewCellItem])

    }

    extension EnglishCodeSentenceCollectionViewSectionModel : SectionModelType {

        

        typealias Item = EnglishCodeSentenceCollectionViewCellItem

        

        var items : [EnglishCodeSentenceCollectionViewCellItem] {

            switch self {

            case .selectedSection(items: let items):

                return items.map {$0}

            case .candidateSection(items: let items):

                return items.map {$0}

            }

        }

        init(original: EnglishCodeSentenceCollectionViewSectionModel, items: [Item]) {

            switch original {

                case .selectedSection(items: let items):

                    self = .selectedSection(items: items)

                case .candidateSection(items: let items):

                    self = .selectedSection(items: items)

            }

        }

        

    }


    섹션을 가진 데이터는 SectionModelType 프로토콜을 준수해야 합니다.

    public protocol SectionModelType {

        associatedtype Item


        var items: [Item] { get }


        init(original: Self, items: [Item])

    }

     


    이 프로토콜을 준수한다고 생각하면 됩니다.


    먼저 기존 코드블럭을 감싼 enum EnglishCodeSentenceCollectionViewCell 을 정의합니다.

    해당 enum은  콜렉션뷰 각각의 셀에 필요한 정보를 담는 모델입니다.


    그 셀을 배열로 하는 items를 연관 값으로 가진 EnglishCodeSentenceCollectionViewSectionModel 을 정의합니다.

    해당 enum은 dataSource를 만들 때 사용할 것입니다.


    class EnglishCodeSentenceViewModel : CodeSentence {

        var selectedCodeBlocks: Variable<[EnglishCodeSentenceCollectionViewCellItem]>

        var candidateCodeBlocks: Variable<[EnglishCodeSentenceCollectionViewCellItem]>

        var answerCodeBlocks: [EnglishCodeBlock]

        

        private let disposeBag = DisposeBag()

        init(answerCodeBlocks : [EnglishCodeBlock], candidateCodeBlocks : [EnglishCodeBlock]){

            self.answerCodeBlocks = answerCodeBlocks

            self.candidateCodeBlocks = Variable(candidateCodeBlocks.map { EnglishCodeSentenceCollectionViewCellItem.selected(cellViewModel: $0) })

            self.selectedCodeBlocks = Variable(EnglishCodeSentenceViewModel.setUpExampleEnglishCodeBlocks().map {

                EnglishCodeSentenceCollectionViewCellItem.selected(cellViewModel: $0)

            })

        }

        convenience init(answerCodeBlocks : [EnglishCodeBlock], candidateCodeBlocks : [EnglishCodeBlock], itemSelected : Driver<IndexPath>){

            self.init(answerCodeBlocks: answerCodeBlocks, candidateCodeBlocks: candidateCodeBlocks)

            itemSelected.drive(onNext: { [unowned self ] indexPath  in

                if (indexPath.section == 0){

                    self.selectedCodeBlocks.value.remove(at: indexPath.item)

                }else {

                    self.candidateCodeBlocks.value.remove(at: indexPath.item)

                }

            }).disposed(by: disposeBag)

        }

        

        static func setUpExampleEnglishCodeBlocks() -> [EnglishCodeBlock]{

            return [EnglishCodeBlock.init(name: "I"), EnglishCodeBlock.init(name: "am") ,

                    EnglishCodeBlock.init(name: "a"), EnglishCodeBlock.init(name: "boy")]

        }

    }


    뷰모델 파트도 바뀌었습니다.

    일단 Variable로 두 섹션에 해당하는 selectedCodeBlocks와 candiateCodeBlocks가 정의되었습니다.

    그에 따라서 [EnglishCodeBlock] 인자를 적절히 Variable<[EnglishCodeSentenceCollectionViewCellItem]>로 변환되도록 매핑해 줍니다.


    class EnglishCodeViewController : UIViewController, UICollectionViewDelegateFlowLayout {

        lazy var collectionView : UICollectionView = {

            let layout = UICollectionViewFlowLayout()

            let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)

            cv.backgroundColor = .white

            return cv

        }()

        

        private let cellIdentifier = "Cell"

        private let disposeBag = DisposeBag()

        

        var viewModel : EnglishCodeSentenceViewModel!

        let dataSource = RxCollectionViewSectionedReloadDataSource<EnglishCodeSentenceCollectionViewSectionModel>(configureCell: { (dataSource, collectionView, indexPath, item) -> UICollectionViewCell in

                let cell =  collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath)

                guard let englishCollectionViewCell : EnglishCodeCollectionViewCell = cell as? EnglishCodeCollectionViewCell else{

                    return cell

                }

                switch dataSource[indexPath] {

                    case let .selected(viewModel):

                        englishCollectionViewCell.codeLabel.text = viewModel.codeBlockName

                    case let .candidate(viewModel, isSelected):

                        englishCollectionViewCell.codeLabel.text = viewModel.codeBlockName

                }

                return englishCollectionViewCell

            }

        )


        

        override func viewDidLoad() {

            setUpViews()

            setUpViewModels()

            setUpCollectionViewBinding()

        }

        func setUpViewModels(){

            viewModel = EnglishCodeSentenceViewModel.init(answerCodeBlocks: [], candidateCodeBlocks: EnglishCodeSentenceViewModel.setUpExampleEnglishCodeBlocks(), itemSelected: collectionView.rx.itemSelected.asDriver())

        }

        func setUpViews(){

            self.view.backgroundColor = UIColor.white

            self.view.addSubview(collectionView)

            collectionView.snp.makeConstraints { (make) in

                make.bottom.top.leading.trailing.equalTo(self.view)

            }

            collectionView.rx.setDelegate(self).disposed(by: disposeBag)

            collectionView.register(EnglishCodeCollectionViewCell.self, forCellWithReuseIdentifier: cellIdentifier)

            

        }

        func setUpCollectionViewBinding(){

            let selected = viewModel.selectedCodeBlocks.asObservable().map { (items) -> EnglishCodeSentenceCollectionViewSectionModel in

              return  .selectedSection(items: items)

            }

            

            let candidate = viewModel.candidateCodeBlocks.asObservable().map { (items) -> EnglishCodeSentenceCollectionViewSectionModel in

                return .candidateSection(items: items)

            }

            

            Observable.combineLatest([selected, candidate])

            .bind(to: collectionView.rx.items(dataSource: dataSource))

            .disposed(by: disposeBag)

        }

    }


    뷰 쪽의 바인딩이 좀 복잡해졌습니다.


    뷰모델에서 두 옵저버를 SectionModel형태로 매핑하고, 그것들 둘을 컴바인하면 Observable[SectionModel] 형태가 됩니다.

    그 결과값을 콜렉션뷰에 바인딩합니다.



    인터넷에 있는 예제와 깃허브의 예제를 조합해 어떻게든 짜 보았습니다만,

    패러다임이 달라서 그런지 짜기가 쉽지가 않습니다.


    콜렉션뷰에서 셀을 옮기거나 셀의 프레임을 계산할 때 등 복잡한 경우를 적절하게 구현하려면 더 공부를 해야할 것 같습니다.


Designed by Tistory.