brunch

You can make anything
by writing

C.S.Lewis

by Tilltue Nov 03. 2016

RxCocoa, UICollectionView

rxdatasource, generic, protocol

* 이글은 Swift 3.0 , RxSwift 3.0.0, RxCocoa 3.0.0, RxDatasource 1.0.0 을 기준으로 작성되었습니다.

* JSON 파싱에는 SwiftyJSON 을 사용했습니다.

https://brunch.co.kr/@tilltue/16 글에 이어서 작성하려 한다.


이 글을 읽기 전에 MVVM 패턴에 대한 이해를 위해 아래의 링크의 글을 읽길 권한다.

https://justhackem.wordpress.com/2017/03/05/mvvm-architectural-pattern


위 작성자 분이 github 에 샘플까지 작성해 두신것도 링크한다.

https://github.com/gyuwon/SwiftMvvmTodoList

본 포스팅도 실제 MVVM 패턴을 구현한 내용이 아닌 바인딩에 집중해져 있기 때문에,

RxCollectionView DataSource 관련 바인딩 기법의 하나로 이해해주었으면 한다.

꼭! 위의 MVVM 관련 글을 읽어보자!

( 아래부터의 포스팅은 나쁜 예제이다 MVVM 패턴으로 설계하고 싶다면 절대 아래와 같이 코딩하지는 말자.

이 글을 쓰던 시점에는 그것을 잘 알지 못했다.

나와 같은 오류를 범하지 않기를 바라며 글을 그대로 남겨둔다. )


CollectionView datasource 의 viewmodel protocol 을 만들어 보려고 한다.

아래와 같은 그림을 생각하자.

CollectionView MVVM

여러 타입의 Model 에 따라 CollectionCellViewModel 을 만들 수 있다. ( Generic Type )

View 는 CollectionView 를 가지며, CollectionViewModel과 바인딩 한다.

CollectionViewModel 은 Data를 로드해서 CellViewModel 을 만들고, CollectionView 의 DataSource 에 바인딩한다. DataSource 를 통해 CollectionCellView 는 CellViewModel과 바인딩 된다.

-정리-

1. 모델은 immutable 하며, View ( ViewController )와 의존이 필요하지 않다.

2. 각 객체들은 내부 컨트롤 로직만 가지며, 바인딩을 통해 값이 전달되고, 스스로 값을 처리한다.


API

NewsApi

class NewsApi {

    class func modelLoad() -> [NewsCellViewModel]? {

        guard let path = Bundle.main.path(forResource: "newsJSON", ofType: "json") else { return nil }

        do {

            let data = try Data(contentsOf: URL(fileURLWithPath: path), options: .alwaysMapped)

            let json =  JSON(data: data)

            let newsViewModels = json["news"].arrayValue.map{ NewsModel(json:$0) }.map{ NewsCellViewModel(newsModel: $0) }

            return newsViewModels

        }catch {

            return nil

        }

    }

}

new를 불러온다. 본래는 네트워크 api 가 위치하겠지만, 편의를 위해 bundle json 을 로드했다.
json 으로 Model 을 만들고, 이를 다시 CellViewModel로 만든다.


Model

NewsModel


struct NewsModel {

    let newsID: Int

    let newsText: String

    init(json: JSON) {

        newsID = json["news_id"].intValue

        newsText = json["text"].stringValue

    }

}

뉴스 모델이다.


View : CollectionView

ViewController

class ViewController: UIViewController {

    @IBOutlet var collectionView: UICollectionView!

    let disposeBag = DisposeBag()

    let triggerSubject = PublishSubject<Void>()

    var collectionViewModel:RxCollectionViewModel<NewsCellViewModel>?

    override func viewDidLoad() {

        super.viewDidLoad()

        collectionViewModel = RxCollectionViewModel<NewsCellViewModel>.init(

                loadTrigger: triggerSubject.asObserver(),

                api: NewsApi.modelLoad,

                bindCollectionView: collectionView,

                disposeBag: disposeBag)

        collectionViewModel?.registerNib(collectionView: collectionView, cellNib: "NewsCollectionCell", cellIdentifier: "NewsCollectionCell")

    }


    override func viewDidAppear(_ animated: Bool) {

        super.viewDidAppear(animated)

        triggerSubject.onNext()

    }

}

ViewController 는 collectionView 와 collectionViewModel을 바인딩 하며,
collectionView에 Cell을 register 한다.
viewmodel load Trigger 를 가지며, 테스트를 위해 ViewDidAppear 에서 trigger 동작을 하도록 구성한다.

* Generic Type은 NewsCellViewModel


View : CollectionViewCell 는 아래쪽에..

NewsCollectionCell
...



ViewModel : CollectionView

RxCollectionViewModelProtocol

protocol RxCollectionViewModelProtocol {

    associatedtype ModelType: IdentifiableType,Equatable

    associatedtype TrackedModelType = AnimatableSectionModel<String,ModelType>

    var trackedCellViewModels: Variable<[TrackedModelType]> { get set }

}


CollectionViewModel 의 프로토콜이다. GenericType 의 ViewModel 을 SectionModel로 만들어서 CollectionView 에 bind 하기위함이다.


RxCollectionViewModel

class RxCollectionViewModel<T:RxCollectionCellViewModel>: RxCollectionViewModelProtocol {

    typealias ModelType = T

    typealias TrackedModelType = AnimatableSectionModel<String,T>

    var loadTrigger: Observable<Void>

    var api: (() -> [ModelType]?)

    var trackedCellViewModels = Variable([TrackedModelType]())

    fileprivate var cellViewModels: Driver<[TrackedModelType]> {

        get {

            return trackedCellViewModels.asDriver().map{ $0 }

        }

    }

    init(loadTrigger: Observable<Void>, api: @escaping (() -> [ModelType]?), bindCollectionView: UICollectionView, disposeBag: DisposeBag ) {

        self.loadTrigger = loadTrigger

        self.api = api

        cellViewModels

            .drive(bindCollectionView.rx.items(dataSource: createCollectionDataSource()))

            .addDisposableTo(disposeBag)

        self.loadTrigger.subscribe(onNext: { [weak self]  in

            if let newsViewModels = self?.api() {

                self?.trackedCellViewModels.value = [AnimatableSectionModel(model: "section0", items:newsViewModels)]

            }

        }, onError: nil, onCompleted: nil, onDisposed: nil).addDisposableTo(disposeBag)

    }

    func registerNib(collectionView: UICollectionView, cellNib: String, cellIdentifier: String) {

        let nib = UINib(nibName: cellNib, bundle: nil)

        collectionView.register(nib, forCellWithReuseIdentifier: cellIdentifier)

    }

    func createCollectionDataSource() -> RxCollectionViewSectionedReloadDataSource<TrackedModelType> {

        let dataSource = RxCollectionViewSectionedReloadDataSource<TrackedModelType>()

        dataSource.configureCell = { ds, cv, ip, cellViewModel -> UICollectionViewCell in

            let cell = cellViewModel.cell(collectionView: cv, indexPath: ip)

            return cell

        }

        return dataSource

    }

}


GenericType < 기본형 : RxCollectionCellViewModel >의 CellViewModel 과,
collectionView 와 trigger , load api, disposeBag 을 전달받는다.

CellViewModel들과 CollectionView 를 bind 하며, 이외 cell resigternib를 등록한다.


ViewModel : CollectionViewCell

RxCollectionCellViewModel

protocol RxCollectionCellViewModel: IdentifiableType, Equatable {

    associatedtype CollectionViewCellType

    var cellNib: String! { get }

    var cellIdentifier: String! { get }

    func cell(collectionView:UICollectionView, indexPath:IndexPath) -> UICollectionViewCell

}

RxCollectionCellViewModel은 뷰를 만들어서 전달해주기 위한 protocol 이다.
nib 와 identifier 는 UICollectionViewCell 을 로드하기 위한 프로퍼티이다.
func cell 은 collectionView 를 받아 셀을 만들어서 반환한다.

* RxDataSource 에서 SectionModel은 아래와 같이 정의되어있다.
AnimatableSectionModel<Section: IdentifiableType, ItemType: IdentifiableType & Equatable>


NewsCellViewModel

struct NewsCellViewModel: RxCollectionCellViewModel {

    typealias CollectionViewCellType = NewsCollectionCell

    var cellNib: String! = "NewsCollectionCell"

    var cellIdentifier: String! = "NewsCollectionCell"

    var newsModel: NewsModel

    var identity: Int {

        return 0

    }

    init(newsModel: NewsModel) {

        self.newsModel = newsModel

    }

    func cell(collectionView:UICollectionView, indexPath:IndexPath) -> UICollectionViewCell {

        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: self.cellIdentifier, for: indexPath) as! CollectionViewCellType

        cell.rx_viewModel.onNext(self)

        return cell

    }

}

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

    return lhs.identity == rhs.identity

}


RxCollectionCellViewModel 를 따르는 viewmodel (cell)이다.
CollectionViewCell 과는 rx_viewModel 를 통해 바인딩 된다.
만약 하나의 뷰 모델에서 여러가지 셀을 만들고 싶다면 cellType 을 typealias 하지 말고, UICollectionViewCell 에 bind 할 수있는 protocol 만들어 사용하는 형태로 하는 것이 좋겠다.


View : CollectionViewCell

NewsCollectionCell

class NewsCollectionCell: UICollectionViewCell {

    @IBOutlet weak var mainTextLabel: UILabel!

    var rx_viewModel: AnyObserver<NewsCellViewModel> {

        return AnyObserver { [weak self] event in

            MainScheduler.ensureExecutingOnScheduler()

            switch event {

            case .next(let value):

                if let strong = self {

                    strong.mainTextLabel.text = "\(value.newsModel.newsID):\(value.newsModel.newsText)"

                }

            default:

                break

            }

        }

    }

}

cellViewModel 과 바인딩 된 CollectionViewCell이다.


예제 실행 결과



Generic 을 통해 여러가지 타입의 뷰모델들을 하나의 CollectionViewModel을 통해 쉽게 바인딩 할 수 있는 방법에 대해서 알아보았다.

브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari