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 을 만들어 보려고 한다.
아래와 같은 그림을 생각하자.
여러 타입의 Model 에 따라 CollectionCellViewModel 을 만들 수 있다. ( Generic Type )
View 는 CollectionView 를 가지며, CollectionViewModel과 바인딩 한다.
CollectionViewModel 은 Data를 로드해서 CellViewModel 을 만들고, CollectionView 의 DataSource 에 바인딩한다. DataSource 를 통해 CollectionCellView 는 CellViewModel과 바인딩 된다.
-정리-
1. 모델은 immutable 하며, View ( ViewController )와 의존이 필요하지 않다.
2. 각 객체들은 내부 컨트롤 로직만 가지며, 바인딩을 통해 값이 전달되고, 스스로 값을 처리한다.
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로 만든다.
NewsModel
struct NewsModel {
let newsID: Int
let newsText: String
init(json: JSON) {
newsID = json["news_id"].intValue
newsText = json["text"].stringValue
}
}
뉴스 모델이다.
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
NewsCollectionCell
...
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를 등록한다.
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 만들어 사용하는 형태로 하는 것이 좋겠다.
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을 통해 쉽게 바인딩 할 수 있는 방법에 대해서 알아보았다.