ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • rxSwift + mvvm 구조로 콜렉션뷰 설계하기 (3)
    iOS/mvvm + RxSwift 스터디 2018. 12. 5. 23:06

    https://sesang06.tistory.com/98


    https://sesang06.tistory.com/99

     

    저번 포스트에서 설게한 콜렉션뷰는 섹션별로 나누어져 있지만,

     아이템을 지울 때 애니메이션 효과가 안 났습니다.


    애니메이션 효과를 나는 콜렉션뷰를 구현해보겠습니다.


    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

    //    }

    //

    //}


    struct EnglishCodeBlock : CodeBlock {

        var codeBlockName: String

        

        var codeBlockId: Int

        init(name: String, id : Int){

            self.codeBlockName = name

            self.codeBlockId = id

        }

    }


    모델 부분은 저번과 별 차이가 없습니다.


    enum EnglishCodeSentenceCollectionViewCellItem {

        case selected(cellViewModel : EnglishCodeBlock)

        case candidate(cellViewModel : EnglishCodeBlock, isSelected : Bool)

    }



    extension EnglishCodeSentenceCollectionViewCellItem : IdentifiableType, Equatable{

        

        typealias Identity = Int

        var identity : Identity {

            

            switch  self {

            case let .selected(cellViewModel: block):

                return block.codeBlockId

            case let .candidate(block, isSelected):

                return isSelected ? block.codeBlockId : -block.codeBlockId

            }

        }

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

            return lhs.identity == rhs.identity

        }

    }

    struct EnglishCodeSentenceCollectionViewSectionModel {

        var index : Int

        var items : [Item]

    }

    extension EnglishCodeSentenceCollectionViewSectionModel : IdentifiableType{

        typealias Identity = Int

        typealias Item = EnglishCodeSentenceCollectionViewCellItem

        var identity : Identity {

           return index

        }

    }

    extension EnglishCodeSentenceCollectionViewSectionModel : AnimatableSectionModelType {

        

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

            self = original

            self.items = items

        }

        

    }


    크게 변한 부분이 셀의 데이터를 가지는 모델 부분입니다.


    rxDataSource가 애니메이션이 진행되는 콜렉션뷰의 섹션 모델은,


    AnimatableSectionModelType 프로토콜을 준수해야 합니다.


    public protocol AnimatableSectionModelType

        : SectionModelType

        , IdentifiableType where Item: IdentifiableType, Item: Equatable {

    }


    섹션 모델 타입에 추가가된 부분이 더 있는데, 

    아이템이 IdentifiableType, Equatable 프로토콜을 준수해야 하며

    섹션모델 타입은 IdentifiableType 프로토콜을 준수해야 함이 그것입니다.



    public protocol IdentifiableType {

        associatedtype Identity: Hashable


        var identity : Identity { get }

    }


    아이덴티어블타입 프토로콜은 해시어블 프로토콜을 더욱 까다롭게 정의한 것으로,

    두 아이템이 다른지 아이디를 통해 식별 가능하도록 한 것입니다.


    예를 들어, 섹션 1은 아이디 1을 가지고 섹션 2가 아이디 2를 가진다고 하면,

    섹션 1의 특정 아이템이 변경되면 Rx 코드가 그것을 자동으로 식별하고, 적절한 애니메이션을 표출해 내주는 역할을 하는 것입니다.


    여기서 문제가 되는 건 EnglishCodeSentenceCollectionViewCellItem 이 어떻게 아이디를 구현하는지에 대한 여부였습니다.


    이 아이디는 섹션 안에서만 독립적이어야 할 뿐만 아니라,

    콜렉션뷰 전체에서 셀 하나가 나타내는 모델이 전부 유니크해야지 코드가 제대로 돌아가더군요.


    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.candidate(cellViewModel: $0, isSelected: false)})

            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 {

                    switch self.candidateCodeBlocks.value[indexPath.item]{

                        case .selected(let _):

                            return

                        case .candidate(let cellViewModel, let isSelected):

                            self.candidateCodeBlocks.value[indexPath.item] = .candidate(cellViewModel: cellViewModel, isSelected: !isSelected)

                        }

                }

            }).disposed(by: disposeBag)

        }

        

        static func setUpExampleEnglishCodeBlocks() -> [EnglishCodeBlock]{

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

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

        }

        static func setUpExampleEnglishCandidateCodeBlocks() -> [EnglishCodeBlock]{

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

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

        }

    }


    class EnglishCodeCollectionViewCell : UICollectionViewCell {

        let codeLabel : UILabel = {

            let label = UILabel()

            label.textAlignment = NSTextAlignment.center

            return label

        }()

        override init(frame: CGRect) {

            super.init(frame: frame)

            setUpViews()

        }

        

        required init?(coder aDecoder: NSCoder) {

            fatalError("init(coder:) has not been implemented")

        }

        func setUpViews(){

            self.addSubview(codeLabel)

            codeLabel.snp.makeConstraints { (make) in

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

            }

        }

    }

    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 = RxCollectionViewSectionedAnimatedDataSource<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

                        englishCollectionViewCell.codeLabel.backgroundColor = UIColor.white

                    case let .candidate(viewModel, isSelected):

                        englishCollectionViewCell.codeLabel.text = viewModel.codeBlockName

                        

                        if (isSelected){

                            englishCollectionViewCell.codeLabel.backgroundColor = UIColor.red

                        }else {

                            englishCollectionViewCell.codeLabel.backgroundColor = UIColor.white

                        }

                }

                return englishCollectionViewCell

            }

        )


        

        override func viewDidLoad() {

            setUpViews()

            setUpViewModels()

            setUpCollectionViewBinding()

        }

        func setUpViewModels(){

            viewModel = EnglishCodeSentenceViewModel.init(answerCodeBlocks: [], candidateCodeBlocks: EnglishCodeSentenceViewModel.setUpExampleEnglishCandidateCodeBlocks(), 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  EnglishCodeSentenceCollectionViewSectionModel(index: 0, items: items)

            }

            

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

                return EnglishCodeSentenceCollectionViewSectionModel(index: 1, items: items)

            }

            

            Observable.combineLatest([selected, candidate])

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

            .disposed(by: disposeBag)

        }

    }



    뷰모델과 뷰는 별다른 차이가 없습니다. 데이터소스가 RxCollectionViewSectionedAnimatedDataSource 로 구현되는 것 정도입니다.


    약간 수정된 전체 코드입니다.


    //

    //  CodeBlock.swift

    //  iOS-todoApp

    //

    //  Created by 세상 on 02/12/2018.

    //  Copyright © 2018 세상. All rights reserved.

    //


    import Foundation

    import RxCocoa

    import RxSwift

    import RxDataSources

    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

    //    }

    //

    //}


    struct EnglishCodeBlock : CodeBlock, Hashable {

        var codeBlockName: String

        var codeBlockId: Int

        init(name: String, id : Int){

            self.codeBlockName = name

            self.codeBlockId = id

        }

    }

    extension EnglishCodeBlock : Equatable {

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

            return lhs.codeBlockName == rhs.codeBlockName && lhs.codeBlockId == rhs.codeBlockId

        }

    }


    struct EnglishCodeSentenceCollectionViewCellItem : Hashable {

        var codeBlock : EnglishCodeBlock

        var isSelected : Bool

    }

    extension EnglishCodeSentenceCollectionViewCellItem : IdentifiableType, Equatable{

        typealias Identity = EnglishCodeSentenceCollectionViewCellItem

        var identity : Identity {

            

           return self

        }

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

            return lhs.codeBlock == rhs.codeBlock && lhs.isSelected == rhs.isSelected

        }

    }

    struct EnglishCodeSentenceCollectionViewSectionModel {

        typealias Identity = Int

        typealias Item = EnglishCodeSentenceCollectionViewCellItem

        

        var index : Int

        var items : [Item]

    }

    extension EnglishCodeSentenceCollectionViewSectionModel : IdentifiableType{

        var identity : Identity {

           return index

        }

        

    }

    extension EnglishCodeSentenceCollectionViewSectionModel : AnimatableSectionModelType {

        

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

            self = original

            self.items = items

        }

        

    }

    //public struct AnimatableSectionModel<Section: IdentifiableType, ItemType: IdentifiableType & Equatable> {

    //    public var model: Section

    //    public var items: [Item]

    //

    //    public init(model: Section, items: [ItemType]) {

    //        self.model = model

    //        self.items = items

    //    }

    //

    //}

    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.init(codeBlock: $0, isSelected: false)})

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

                EnglishCodeSentenceCollectionViewCellItem.init(codeBlock: $0, isSelected: false)

            })

        }

        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[indexPath.item].isSelected = !self.candidateCodeBlocks.value[indexPath.item].isSelected

                }

            }).disposed(by: disposeBag)

        }

        

        static func setUpExampleEnglishCodeBlocks() -> [EnglishCodeBlock]{

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

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

        }

        static func setUpExampleEnglishCandidateCodeBlocks() -> [EnglishCodeBlock]{

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

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

        }

    }


    class EnglishCodeCollectionViewCell : UICollectionViewCell {

        let codeLabel : UILabel = {

            let label = UILabel()

            label.textAlignment = NSTextAlignment.center

            return label

        }()

        override init(frame: CGRect) {

            super.init(frame: frame)

            setUpViews()

        }

        

        required init?(coder aDecoder: NSCoder) {

            fatalError("init(coder:) has not been implemented")

        }

        func setUpViews(){

            self.addSubview(codeLabel)

            codeLabel.snp.makeConstraints { (make) in

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

            }

        }

    }

    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 = RxCollectionViewSectionedAnimatedDataSource<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

                }

                let item = dataSource[indexPath]

            

                if (item.isSelected){

                    englishCollectionViewCell.codeLabel.backgroundColor = UIColor.red

                }else {

                    englishCollectionViewCell.codeLabel.backgroundColor = UIColor.white

                }

                englishCollectionViewCell.codeLabel.text = item.codeBlock.codeBlockName

                return englishCollectionViewCell

            }

        )


        

        override func viewDidLoad() {

            setUpViews()

            setUpViewModels()

            setUpCollectionViewBinding()

        }

        func setUpViewModels(){

            viewModel = EnglishCodeSentenceViewModel.init(answerCodeBlocks: [], candidateCodeBlocks: EnglishCodeSentenceViewModel.setUpExampleEnglishCandidateCodeBlocks(), 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  EnglishCodeSentenceCollectionViewSectionModel(index: 0, items: items)

            }

            

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

                return EnglishCodeSentenceCollectionViewSectionModel(index: 1, items: items)

            }

            

            Observable.combineLatest([selected, candidate])

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

            .disposed(by: disposeBag)

        }

    }



    //        viewModel.selectedCodeBlocks.asObservable()

    ////            .bind(to: collectionView.rx.items(cellIdentifier: cellIdentifier)) {  row, element, cell in

    ////                // = element.codeBlockName

    ////                guard let englishCollectionViewCell : EnglishCodeCollectionViewCell = cell as! EnglishCodeCollectionViewCell else{

    ////                    return

    ////                }

    ////                englishCollectionViewCell.codeLabel.text = element.codeBlockName

    ////

    ////            }

    //            .disposed(by: disposeBag)

    //       Observable.combineLatest(viewModel.candidateCodeBlocks.asObservable(), viewModel.selectedCodeBlocks.asObservable())

    //        .bind(to: collectionView.rx.items(dataSource: viewModel.dataSouce))

    //         .disposed(by: disposeBag)

    //        let sections : [EnglishCodeSentenceCollectionViewSectionModel] = [

    //            viewModel.selectedCodeBlocks.asObservable().map()

    //        ]


Designed by Tistory.