brunch

RxCocoa, UICollectionView

rxdatasource, generic, protocol

by Tilltue

* 이글은 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을 통해 쉽게 바인딩 할 수 있는 방법에 대해서 알아보았다.

keyword
매거진의 이전글Firebase #iOS, NewsFeed 기초