-
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()
// ]
반응형'iOS > mvvm + RxSwift 스터디' 카테고리의 다른 글
RxSwift에서 combineLatest 와 withLatestFrom 차이 이해하기 (0) 2019.03.03 RxSwift 강좌 (0) 2018.12.07 RxSwift 슬랙 가입 (0) 2018.12.06 rxSwift + mvvm 구조로 콜렉션뷰 설계하기 (2) (0) 2018.12.04 rxSwift + mvvm 구조로 콜렉션뷰 설계하기 (0) 2018.12.04