SlideShare una empresa de Scribd logo
1 de 35
Descargar para leer sin conexión
class CardTableViewCell: UITableViewCell
class ProductCardView: UIViewclass CardTableViewCell: UITableViewCell
protocol CardViewModelType {
var cardType: CardType { get }
var title: String? { get }
var subTitle: String? { get }
var rating: Double? { get }
var reviewCount: Int? { get }
var profileImageButtonViewModel: ProfileImageButtonViewModelType? { get }
var coverImageURLString: String? { get }
var tags: [String]? { get }
// Input
var didWish: PublishSubject<Bool>? { get }
// Output
var wish: Driver<Bool>? { get }
var disposeBag: DisposeBag { get }
}
enum CardType {
case small, big
var cellSize: CGSize {
switch self {
case .small:
return CGSize(width: 160, height: 200)
case .big:
return CGSize(width: 250, height: 230)
}
}
}
struct CardViewModel: CardViewModelType {
init(cardType: CardType, product: Product) {
self.cardType = cardType
self.title = product.title
self.subTitle = product.catchPhrase
self.coverImageURLString = product?.imageURLs?.first
self.profileImageButtonViewModel = {
guard let host = product.host else { return nil }
return ProfileImageButtonViewModel(profileType: .host(host), size:
cardType.circleImageSize)
}()
self.rating = product.rating
self.reviewCount = product.reviewCount
self.tags = {
if let tags = product.areaTags?
.filter({ $0.label?.characters.count ?? 0 > 0 })
.map({ $0.label ?? "" }),
tags.count > 0 { return tags }
else if let tags = product.locationTags?
.filter({ $0.label?.characters.count ?? 0 > 0 })
.map({ $0.label ?? "" }),
tags.count > 0 { return tags }
else { return nil }
}()
...
...
if let product = product, let productId = product.id {
self.wish = self.didWish?.asDriver(onErrorJustReturn: false)
.withLatestFrom(Driver.just(productId)) { ($0, $1) }
.flatMap { w, pId in
Router.ProductWishToggle(["productId": pId]).request
.rx.json()
.asDriver(onErrorJustReturn: [:])
.map { (JSON($0)["success"].bool ?? false) ? !w : w }
}
.startWith(product.isWished)
// Sync wishes
self.wish?.withLatestFrom(Driver.just(product)) { ($0, $1) }
.drive(onNext: { $0.1.isWished = $0.0 })
.addDisposableTo(self.disposeBag)
} else {
self.wish = nil
}
}
}
protocol CardViewType {
var coverImageView: UIImageView? { get }
...
}
extension CardType: CardViewType {
var coverImageView: UIImageView? {
switch self {
case .big:
let imageView = UIImageView(frame: self.coverImageSize.rect)
...
return imageView
case .small:
let imageView = UIImageView(frame: self.coverImageSize.rect)
...
return imageView
}
}
...
}
final class CardView: UIView, CardViewType {
let coverImageView: UIImageView?
...
private var disposeBag: DisposeBag
required init(on superview: UIView, with viewModel: CardViewModelType,
inset: UIEdgeInsets = .zero) {
self.coverImageView = viewModel.cardType.coverImageView
...
super.init(frame: viewModel.cardType.cellSize.rect)
superview.addSubview(self)
self.snp.makeConstraints {
$0.size.equalTo(viewModel.cardType.cellSize)
$0.edges.equalTo(superview.snp.edges).inset(inset)
}
self.configure(by: viewModel)
self.configureLayout(by: viewModel)
}
...
func configure(by viewModel: CardViewModelType) {
self.disposeBag = viewModel.disposeBag
self.coverImageView?.setImage(
with: viewModel.coverImageURLString,
transformation: viewModel.cardType.coverImageSize.cloudinaryTransformation
)
...
self.favoriteButton?.removeTarget(self, action: nil, for: .allEvents)
if let favoriteButton = self.favoriteButton {
let needUserName = User.notification
.map { $0.name?.isEmpty ?? true }
let tapFollowButton = favoriteButton.rx.tap.asDriver()
.withLatestFrom(needUserName) { $1 }
.flatMap { needUserName -> Driver<Bool> in
guard needUserName else { return Driver.just(false) }
return User.noNameAlert.rx.alert().asDriver(onErrorJustReturn: false)
}
tapFollowButton
.filter { $0 }.map { _ in }
.drive(ProfileViewController.present)
.addDisposableTo(self.disposeBag)
tapFollowButton
.filter { !$0 }
.withLatestFrom(viewModel.wish!) { $1 }
.drive(viewModel.didWish!)
.addDisposableTo(self.disposeBag)
viewModel.wish?
.drive(favoriteButton.rx.isSelected)
.addDisposableTo(self.disposeBag)
}
}
...
...
private func configureLayout(by viewModel: CardViewModelType) {
switch viewModel.cardType {
case .big: self.configureLayoutForBig(by: viewModel)
case .small: self.configureLayoutForSmall(by: viewModel)
}
}
private func configureLayoutForBig(by viewModel: CardViewModelType) {
// Construct Views
self.addSubviews([self.coverImageView, ...])
...
// Layout Views
self.coverImageView?.snp.makeConstraints {
$0.size.equalTo(viewModel.cardType.coverImageSize)
$0.top.equalToSuperview()
$0.left.equalToSuperview()
$0.right.equalToSuperview()
}
...
}
private func configureLayoutForSmall(by viewModel: CardViewModelType) {
...
}
}
protocol CardViewConatinerType {
var cardView: CardViewType? { get }
func configure(with cardViewModel: CardViewModelType)
}
class CardCollectionViewCell: UICollectionViewCell, CardViewContainerType {
var cardView: CardViewType? {
return self.contentView.subviews.first as? CardViewType
}
func configure(with cardViewModel: CardViewModelType) {
guard let cardView = self.cardView else {
let _ = CardView(on: self.contentView, with: cardViewModel)
// Initialize
return
}
cardView.configure(by: cardViewModel)
}
override func prepareForReuse() {
super.prepareForReuse()
self.cardView?.coverImageView?.image = nil
self.cardView?.profileImageButton?.setImage(nil, for: .normal)
}
}
class CardTableViewCell: UITableViewCell, CardViewContainerType {
var cardView: CardViewType? {
return self.contentView.subviews.first as? CardViewType
}
func configure(with cardViewModel: CardViewModelType) {
guard let cardView = self.cardView else {
let cardView = CardView(on: self.contentView, with:
cardViewModel, inset: Metric.cellInset)
self.backgroundColor = UIColor.clear
cardView.borderColor = UIColor.lightblue
cardView.borderWidth = 1
return
}
cardView.configure(by: cardViewModel)
}
override func prepareForReuse() {
super.prepareForReuse()
self.cardView?.coverImageView?.image = nil
self.cardView?.profileImageButton?.setImage(nil, for: .normal)
}
}
let identifier = CardCollectionViewCell.className
let cell: CardCollectionViewCell = collectionView
.dequeueReusableCell(
withReuseIdentifier: identifier,
for: indexPath
) as! CardCollectionViewCell
let viewModel = CardViewModel(
cardType: item.cardType,
data: item.data
)
cell.configure(with: viewModel)
return cell
enum CardType {
case small, big
var cellSize: CGSize {
switch self {
case .small:
return CGSize(width: 160, height: 200)
case .big:
return CGSize(width: 250, height: 230)
}
}
}
enum CardType {
case small, big, realFinalISwearGodFinalType
var cellSize: CGSize {
switch self {
case .small:
return CGSize(width: 160, height: 200)
case .big:
return CGSize(width: 250, height: 230)
case .realFinalISwearGodFinalType:
return CGSize(width: 320, height: 100)
}
}
}
protocol CardViewModelType {
var cardType: CardType { get }
var title: String? { get }
var subTitle: String? { get }
var rating: Double? { get }
var reviewCount: Int? { get }
var profileImageButtonViewModel: ProfileImageButtonViewModelType? { get }
var coverImageURLString: String? { get }
var tags: [String]? { get }
// Input
var didWish: PublishSubject<Bool>? { get }
// Output
var wish: Driver<Bool>? { get }
var disposeBag: DisposeBag { get }
}
protocol CardViewType {
var coverImageView: UIImageView? { get }
...
}
extension CardType: CardViewType {
var coverImageView: UIImageView? {
switch self {
case .big:
let imageView = UIImageView(frame: self.coverImageSize.rect)
...
return imageView
case .small:
let imageView = UIImageView(frame: self.coverImageSize.rect)
...
return imageView
case .realFinalISwearGodFinalType:
return nil
}
}
...
}
final class CardView: UIView, CardViewType {
let coverImageView: UIImageView?
...
private var disposeBag: DisposeBag
required init(on superview: UIView, with viewModel: CardViewModelType,
inset: UIEdgeInsets = .zero) {
self.coverImageView = viewModel.cardType.coverImageView
...
super.init(frame: viewModel.cardType.cellSize.rect)
superview.addSubview(self)
self.snp.makeConstraints {
$0.size.equalTo(viewModel.cardType.cellSize)
$0.edges.equalTo(superview.snp.edges).inset(inset)
}
self.configure(by: viewModel)
self.configureLayout(by: viewModel)
}
...
func configure(by viewModel: CardViewModelType) {
self.disposeBag = viewModel.disposeBag
self.coverImageView?.setImage(
with: viewModel.coverImageURLString,
transformation: viewModel.cardType.coverImageSize.cloudinaryTransformation
)
...
self.favoriteButton?.removeTarget(self, action: nil, for: .allEvents)
if let favoriteButton = self.favoriteButton {
let needUserName = User.notification
.map { $0.name?.isEmpty ?? true }
let tapFollowButton = favoriteButton.rx.tap.asDriver()
.withLatestFrom(needUserName) { $1 }
.flatMap { needUserName -> Driver<Bool> in
guard needUserName else { return Driver.just(false) }
return User.noNameAlert.rx.alert().asDriver(onErrorJustReturn: false)
}
tapFollowButton
.filter { $0 }.map { _ in }
.drive(ProfileViewController.present)
.addDisposableTo(self.disposeBag)
tapFollowButton
.filter { !$0 }
.withLatestFrom(viewModel.wish!) { $1 }
.drive(viewModel.didWish!)
.addDisposableTo(self.disposeBag)
viewModel.wish?
.drive(favoriteButton.rx.isSelected)
.addDisposableTo(self.disposeBag)
}
}
...
...
private func configureLayout(by viewModel: CardViewModelType) {
switch viewModel.cardType {
case .big: self.configureLayoutForBig(by: viewModel)
case .small: self.configureLayoutForSmall(by: viewModel)
case .realFinalISwearGodFinalType: self.configureLayoutForFinal(by:
viewModel)
}
}
private func configureLayoutForFinal(by viewModel: CardViewModelType) {
...
}
private func configureLayoutForBig(by viewModel: CardViewModelType) {
...
}
private func configureLayoutForSmall(by viewModel: CardViewModelType) {
...
}
}
final class CardButton: UIView, CardViewContainerType {
var cardView: CardViewType? {
return self.subviews.first as? CardViewType
}
func configure(with cardViewModel: CardViewModelType) {
guard let cardView = self.cardView else {
let cardView = CardView(on: self, with: cardViewModel)
cardView.isUserInteractionEnabled = false
return
}
cardView.configure(by: cardViewModel)
}
required init(cardViewModel: CardViewModelType) {
super.init(frame: cardViewModel.cardType.cellSize.rect))
self.configure(with: cardViewModel)
}
}
let viewModel = CardViewModel(cardType: .realFinalISwearGodFinalType, data: $0)
let button = CardButton(cardViewModel: viewModel)
self.stackView.addArrangedSubview(button)
기획, 디자인 변경에 강한 카드뷰 만들기 - iOS Tech Talk 2017

Más contenido relacionado

La actualidad más candente (6)

Symfony2. Form and Validation
Symfony2. Form and ValidationSymfony2. Form and Validation
Symfony2. Form and Validation
 
Design strategies for AngularJS
Design strategies for AngularJSDesign strategies for AngularJS
Design strategies for AngularJS
 
Image recognition applications and dataset preparation - DevFest Baghdad 2018
Image recognition applications and dataset preparation - DevFest Baghdad 2018Image recognition applications and dataset preparation - DevFest Baghdad 2018
Image recognition applications and dataset preparation - DevFest Baghdad 2018
 
How to build a html5 websites.v1
How to build a html5 websites.v1How to build a html5 websites.v1
How to build a html5 websites.v1
 
course js day 3
course js day 3course js day 3
course js day 3
 
Jquery In Rails
Jquery In RailsJquery In Rails
Jquery In Rails
 

Destacado

Viene el rio recitando power point1
Viene el rio recitando power point1Viene el rio recitando power point1
Viene el rio recitando power point1
musimusikera1
 
Periodontal disease
Periodontal diseasePeriodontal disease
Periodontal disease
vmuf
 
Ayak Bileği Kırıklarında Artroskopinin Yeri
Ayak Bileği Kırıklarında Artroskopinin YeriAyak Bileği Kırıklarında Artroskopinin Yeri
Ayak Bileği Kırıklarında Artroskopinin Yeri
Ayak ve Bilek Cerrahisi
 

Destacado (14)

ETIP PV conference: 'Photovoltaics: centre-stage in the power system
ETIP PV conference: 'Photovoltaics: centre-stage in the power systemETIP PV conference: 'Photovoltaics: centre-stage in the power system
ETIP PV conference: 'Photovoltaics: centre-stage in the power system
 
Bajo eléctrico
Bajo eléctricoBajo eléctrico
Bajo eléctrico
 
Some Major Facts of Eastern Europe Sourcing
 Some Major Facts of Eastern Europe Sourcing Some Major Facts of Eastern Europe Sourcing
Some Major Facts of Eastern Europe Sourcing
 
Embriologia
EmbriologiaEmbriologia
Embriologia
 
CORE_monogram
CORE_monogramCORE_monogram
CORE_monogram
 
20160406115950221
2016040611595022120160406115950221
20160406115950221
 
Maaseuturahaston hankekoulutus 14.1.2016, maksatusosio
Maaseuturahaston hankekoulutus 14.1.2016, maksatusosioMaaseuturahaston hankekoulutus 14.1.2016, maksatusosio
Maaseuturahaston hankekoulutus 14.1.2016, maksatusosio
 
Viene el rio recitando power point1
Viene el rio recitando power point1Viene el rio recitando power point1
Viene el rio recitando power point1
 
Periodontal disease
Periodontal diseasePeriodontal disease
Periodontal disease
 
Ayak Bileği Kırıklarında Artroskopinin Yeri
Ayak Bileği Kırıklarında Artroskopinin YeriAyak Bileği Kırıklarında Artroskopinin Yeri
Ayak Bileği Kırıklarında Artroskopinin Yeri
 
Wie Sie Ihr Unternehmen vor Cyber-Attacken schützen können - webinar 2017, de...
Wie Sie Ihr Unternehmen vor Cyber-Attacken schützen können - webinar 2017, de...Wie Sie Ihr Unternehmen vor Cyber-Attacken schützen können - webinar 2017, de...
Wie Sie Ihr Unternehmen vor Cyber-Attacken schützen können - webinar 2017, de...
 
Componentes actuales de una microcomputadora y sus periféricos
Componentes actuales de una microcomputadora y sus periféricosComponentes actuales de una microcomputadora y sus periféricos
Componentes actuales de una microcomputadora y sus periféricos
 
Noma
NomaNoma
Noma
 
Askozia VoIP Security white paper - 2017, English
Askozia VoIP Security white paper - 2017, EnglishAskozia VoIP Security white paper - 2017, English
Askozia VoIP Security white paper - 2017, English
 

Similar a 기획, 디자인 변경에 강한 카드뷰 만들기 - iOS Tech Talk 2017

Jarv.us Showcase — SenchaCon 2011
Jarv.us Showcase — SenchaCon 2011Jarv.us Showcase — SenchaCon 2011
Jarv.us Showcase — SenchaCon 2011
Chris Alfano
 
Optional. Tips and Tricks - UA Mobile 2019
Optional. Tips and Tricks - UA Mobile 2019Optional. Tips and Tricks - UA Mobile 2019
Optional. Tips and Tricks - UA Mobile 2019
UA Mobile
 
前端MVC 豆瓣说
前端MVC 豆瓣说前端MVC 豆瓣说
前端MVC 豆瓣说
Ting Lv
 
Th 0230 turbo_chargeyourui-howtomakeyourandroidu_ifastandefficient
Th 0230 turbo_chargeyourui-howtomakeyourandroidu_ifastandefficientTh 0230 turbo_chargeyourui-howtomakeyourandroidu_ifastandefficient
Th 0230 turbo_chargeyourui-howtomakeyourandroidu_ifastandefficient
Bin Shao
 

Similar a 기획, 디자인 변경에 강한 카드뷰 만들기 - iOS Tech Talk 2017 (20)

[22]Efficient and Testable MVVM pattern
[22]Efficient and Testable MVVM pattern[22]Efficient and Testable MVVM pattern
[22]Efficient and Testable MVVM pattern
 
Jarv.us Showcase — SenchaCon 2011
Jarv.us Showcase — SenchaCon 2011Jarv.us Showcase — SenchaCon 2011
Jarv.us Showcase — SenchaCon 2011
 
CakePHP in iPhone App
CakePHP in iPhone AppCakePHP in iPhone App
CakePHP in iPhone App
 
“iOS 11 в App in the Air”, Пронин Сергей, App in the Air
“iOS 11 в App in the Air”, Пронин Сергей, App in the Air“iOS 11 в App in the Air”, Пронин Сергей, App in the Air
“iOS 11 в App in the Air”, Пронин Сергей, App in the Air
 
Optional. Tips and Tricks - UA Mobile 2019
Optional. Tips and Tricks - UA Mobile 2019Optional. Tips and Tricks - UA Mobile 2019
Optional. Tips and Tricks - UA Mobile 2019
 
Vue business first
Vue business firstVue business first
Vue business first
 
前端MVC 豆瓣说
前端MVC 豆瓣说前端MVC 豆瓣说
前端MVC 豆瓣说
 
Soa lab 3
Soa lab 3Soa lab 3
Soa lab 3
 
Creating an Uber Clone - Part XXIV - Transcript.pdf
Creating an Uber Clone - Part XXIV - Transcript.pdfCreating an Uber Clone - Part XXIV - Transcript.pdf
Creating an Uber Clone - Part XXIV - Transcript.pdf
 
Developing Google Glass
Developing Google GlassDeveloping Google Glass
Developing Google Glass
 
Th 0230 turbo_chargeyourui-howtomakeyourandroidu_ifastandefficient
Th 0230 turbo_chargeyourui-howtomakeyourandroidu_ifastandefficientTh 0230 turbo_chargeyourui-howtomakeyourandroidu_ifastandefficient
Th 0230 turbo_chargeyourui-howtomakeyourandroidu_ifastandefficient
 
Arquitetando seu aplicativo Android com Jetpack
Arquitetando seu aplicativo Android com JetpackArquitetando seu aplicativo Android com Jetpack
Arquitetando seu aplicativo Android com Jetpack
 
Does testability imply good design - Andrzej Jóźwiak - TomTom Dev Day 2022
Does testability imply good design - Andrzej Jóźwiak - TomTom Dev Day 2022Does testability imply good design - Andrzej Jóźwiak - TomTom Dev Day 2022
Does testability imply good design - Andrzej Jóźwiak - TomTom Dev Day 2022
 
NongBeer MVP Demo application
NongBeer MVP Demo applicationNongBeer MVP Demo application
NongBeer MVP Demo application
 
Prototype UI
Prototype UIPrototype UI
Prototype UI
 
Griffon @ Svwjug
Griffon @ SvwjugGriffon @ Svwjug
Griffon @ Svwjug
 
SenchaCon 2016: Add Magic to Your Ext JS Apps with D3 Visualizations - Vitaly...
SenchaCon 2016: Add Magic to Your Ext JS Apps with D3 Visualizations - Vitaly...SenchaCon 2016: Add Magic to Your Ext JS Apps with D3 Visualizations - Vitaly...
SenchaCon 2016: Add Magic to Your Ext JS Apps with D3 Visualizations - Vitaly...
 
Practical Model View Programming (Roadshow Version)
Practical Model View Programming (Roadshow Version)Practical Model View Programming (Roadshow Version)
Practical Model View Programming (Roadshow Version)
 
Getting Started with Combine And SwiftUI
Getting Started with Combine And SwiftUIGetting Started with Combine And SwiftUI
Getting Started with Combine And SwiftUI
 
PWA night vol.11 20191218
PWA night vol.11 20191218PWA night vol.11 20191218
PWA night vol.11 20191218
 

Más de Wanbok Choi

06 멀티뷰 애플리케이션
06 멀티뷰 애플리케이션06 멀티뷰 애플리케이션
06 멀티뷰 애플리케이션
Wanbok Choi
 
04 안드로이드 응용프로그램의 구조
04 안드로이드 응용프로그램의 구조04 안드로이드 응용프로그램의 구조
04 안드로이드 응용프로그램의 구조
Wanbok Choi
 

Más de Wanbok Choi (9)

[Let'Swift 2019] 실용적인 함수형 프로그래밍 워크샵
[Let'Swift 2019] 실용적인 함수형 프로그래밍 워크샵[Let'Swift 2019] 실용적인 함수형 프로그래밍 워크샵
[Let'Swift 2019] 실용적인 함수형 프로그래밍 워크샵
 
WWDC 2019 Cheatsheet
WWDC 2019 CheatsheetWWDC 2019 Cheatsheet
WWDC 2019 Cheatsheet
 
iOS 개발자의 Flutter 체험기
iOS 개발자의 Flutter 체험기iOS 개발자의 Flutter 체험기
iOS 개발자의 Flutter 체험기
 
[이모콘 2018 S/S] Swift로 코인 트레이딩 봇 만들기
[이모콘 2018 S/S] Swift로 코인 트레이딩 봇 만들기[이모콘 2018 S/S] Swift로 코인 트레이딩 봇 만들기
[이모콘 2018 S/S] Swift로 코인 트레이딩 봇 만들기
 
RxSwift 활용하기 - Let'Swift 2017
RxSwift 활용하기 - Let'Swift 2017RxSwift 활용하기 - Let'Swift 2017
RxSwift 활용하기 - Let'Swift 2017
 
try! Swift Tokyo 2017 후기
try! Swift Tokyo 2017 후기try! Swift Tokyo 2017 후기
try! Swift Tokyo 2017 후기
 
LetSwift RxSwift 시작하기
LetSwift RxSwift 시작하기LetSwift RxSwift 시작하기
LetSwift RxSwift 시작하기
 
06 멀티뷰 애플리케이션
06 멀티뷰 애플리케이션06 멀티뷰 애플리케이션
06 멀티뷰 애플리케이션
 
04 안드로이드 응용프로그램의 구조
04 안드로이드 응용프로그램의 구조04 안드로이드 응용프로그램의 구조
04 안드로이드 응용프로그램의 구조
 

기획, 디자인 변경에 강한 카드뷰 만들기 - iOS Tech Talk 2017

  • 1.
  • 2.
  • 3.
  • 4.
  • 6. class ProductCardView: UIViewclass CardTableViewCell: UITableViewCell
  • 7.
  • 8.
  • 9.
  • 10. protocol CardViewModelType { var cardType: CardType { get } var title: String? { get } var subTitle: String? { get } var rating: Double? { get } var reviewCount: Int? { get } var profileImageButtonViewModel: ProfileImageButtonViewModelType? { get } var coverImageURLString: String? { get } var tags: [String]? { get } // Input var didWish: PublishSubject<Bool>? { get } // Output var wish: Driver<Bool>? { get } var disposeBag: DisposeBag { get } } enum CardType { case small, big var cellSize: CGSize { switch self { case .small: return CGSize(width: 160, height: 200) case .big: return CGSize(width: 250, height: 230) } } }
  • 11. struct CardViewModel: CardViewModelType { init(cardType: CardType, product: Product) { self.cardType = cardType self.title = product.title self.subTitle = product.catchPhrase self.coverImageURLString = product?.imageURLs?.first self.profileImageButtonViewModel = { guard let host = product.host else { return nil } return ProfileImageButtonViewModel(profileType: .host(host), size: cardType.circleImageSize) }() self.rating = product.rating self.reviewCount = product.reviewCount self.tags = { if let tags = product.areaTags? .filter({ $0.label?.characters.count ?? 0 > 0 }) .map({ $0.label ?? "" }), tags.count > 0 { return tags } else if let tags = product.locationTags? .filter({ $0.label?.characters.count ?? 0 > 0 }) .map({ $0.label ?? "" }), tags.count > 0 { return tags } else { return nil } }() ...
  • 12. ... if let product = product, let productId = product.id { self.wish = self.didWish?.asDriver(onErrorJustReturn: false) .withLatestFrom(Driver.just(productId)) { ($0, $1) } .flatMap { w, pId in Router.ProductWishToggle(["productId": pId]).request .rx.json() .asDriver(onErrorJustReturn: [:]) .map { (JSON($0)["success"].bool ?? false) ? !w : w } } .startWith(product.isWished) // Sync wishes self.wish?.withLatestFrom(Driver.just(product)) { ($0, $1) } .drive(onNext: { $0.1.isWished = $0.0 }) .addDisposableTo(self.disposeBag) } else { self.wish = nil } } }
  • 13.
  • 14. protocol CardViewType { var coverImageView: UIImageView? { get } ... } extension CardType: CardViewType { var coverImageView: UIImageView? { switch self { case .big: let imageView = UIImageView(frame: self.coverImageSize.rect) ... return imageView case .small: let imageView = UIImageView(frame: self.coverImageSize.rect) ... return imageView } } ... }
  • 15. final class CardView: UIView, CardViewType { let coverImageView: UIImageView? ... private var disposeBag: DisposeBag required init(on superview: UIView, with viewModel: CardViewModelType, inset: UIEdgeInsets = .zero) { self.coverImageView = viewModel.cardType.coverImageView ... super.init(frame: viewModel.cardType.cellSize.rect) superview.addSubview(self) self.snp.makeConstraints { $0.size.equalTo(viewModel.cardType.cellSize) $0.edges.equalTo(superview.snp.edges).inset(inset) } self.configure(by: viewModel) self.configureLayout(by: viewModel) } ...
  • 16. func configure(by viewModel: CardViewModelType) { self.disposeBag = viewModel.disposeBag self.coverImageView?.setImage( with: viewModel.coverImageURLString, transformation: viewModel.cardType.coverImageSize.cloudinaryTransformation ) ... self.favoriteButton?.removeTarget(self, action: nil, for: .allEvents) if let favoriteButton = self.favoriteButton { let needUserName = User.notification .map { $0.name?.isEmpty ?? true } let tapFollowButton = favoriteButton.rx.tap.asDriver() .withLatestFrom(needUserName) { $1 } .flatMap { needUserName -> Driver<Bool> in guard needUserName else { return Driver.just(false) } return User.noNameAlert.rx.alert().asDriver(onErrorJustReturn: false) } tapFollowButton .filter { $0 }.map { _ in } .drive(ProfileViewController.present) .addDisposableTo(self.disposeBag) tapFollowButton .filter { !$0 } .withLatestFrom(viewModel.wish!) { $1 } .drive(viewModel.didWish!) .addDisposableTo(self.disposeBag) viewModel.wish? .drive(favoriteButton.rx.isSelected) .addDisposableTo(self.disposeBag) } } ...
  • 17. ... private func configureLayout(by viewModel: CardViewModelType) { switch viewModel.cardType { case .big: self.configureLayoutForBig(by: viewModel) case .small: self.configureLayoutForSmall(by: viewModel) } } private func configureLayoutForBig(by viewModel: CardViewModelType) { // Construct Views self.addSubviews([self.coverImageView, ...]) ... // Layout Views self.coverImageView?.snp.makeConstraints { $0.size.equalTo(viewModel.cardType.coverImageSize) $0.top.equalToSuperview() $0.left.equalToSuperview() $0.right.equalToSuperview() } ... } private func configureLayoutForSmall(by viewModel: CardViewModelType) { ... } }
  • 18.
  • 19. protocol CardViewConatinerType { var cardView: CardViewType? { get } func configure(with cardViewModel: CardViewModelType) } class CardCollectionViewCell: UICollectionViewCell, CardViewContainerType { var cardView: CardViewType? { return self.contentView.subviews.first as? CardViewType } func configure(with cardViewModel: CardViewModelType) { guard let cardView = self.cardView else { let _ = CardView(on: self.contentView, with: cardViewModel) // Initialize return } cardView.configure(by: cardViewModel) } override func prepareForReuse() { super.prepareForReuse() self.cardView?.coverImageView?.image = nil self.cardView?.profileImageButton?.setImage(nil, for: .normal) } }
  • 20. class CardTableViewCell: UITableViewCell, CardViewContainerType { var cardView: CardViewType? { return self.contentView.subviews.first as? CardViewType } func configure(with cardViewModel: CardViewModelType) { guard let cardView = self.cardView else { let cardView = CardView(on: self.contentView, with: cardViewModel, inset: Metric.cellInset) self.backgroundColor = UIColor.clear cardView.borderColor = UIColor.lightblue cardView.borderWidth = 1 return } cardView.configure(by: cardViewModel) } override func prepareForReuse() { super.prepareForReuse() self.cardView?.coverImageView?.image = nil self.cardView?.profileImageButton?.setImage(nil, for: .normal) } }
  • 21. let identifier = CardCollectionViewCell.className let cell: CardCollectionViewCell = collectionView .dequeueReusableCell( withReuseIdentifier: identifier, for: indexPath ) as! CardCollectionViewCell let viewModel = CardViewModel( cardType: item.cardType, data: item.data ) cell.configure(with: viewModel) return cell
  • 22.
  • 23.
  • 24.
  • 25. enum CardType { case small, big var cellSize: CGSize { switch self { case .small: return CGSize(width: 160, height: 200) case .big: return CGSize(width: 250, height: 230) } } }
  • 26. enum CardType { case small, big, realFinalISwearGodFinalType var cellSize: CGSize { switch self { case .small: return CGSize(width: 160, height: 200) case .big: return CGSize(width: 250, height: 230) case .realFinalISwearGodFinalType: return CGSize(width: 320, height: 100) } } }
  • 27. protocol CardViewModelType { var cardType: CardType { get } var title: String? { get } var subTitle: String? { get } var rating: Double? { get } var reviewCount: Int? { get } var profileImageButtonViewModel: ProfileImageButtonViewModelType? { get } var coverImageURLString: String? { get } var tags: [String]? { get } // Input var didWish: PublishSubject<Bool>? { get } // Output var wish: Driver<Bool>? { get } var disposeBag: DisposeBag { get } }
  • 28. protocol CardViewType { var coverImageView: UIImageView? { get } ... } extension CardType: CardViewType { var coverImageView: UIImageView? { switch self { case .big: let imageView = UIImageView(frame: self.coverImageSize.rect) ... return imageView case .small: let imageView = UIImageView(frame: self.coverImageSize.rect) ... return imageView case .realFinalISwearGodFinalType: return nil } } ... }
  • 29.
  • 30. final class CardView: UIView, CardViewType { let coverImageView: UIImageView? ... private var disposeBag: DisposeBag required init(on superview: UIView, with viewModel: CardViewModelType, inset: UIEdgeInsets = .zero) { self.coverImageView = viewModel.cardType.coverImageView ... super.init(frame: viewModel.cardType.cellSize.rect) superview.addSubview(self) self.snp.makeConstraints { $0.size.equalTo(viewModel.cardType.cellSize) $0.edges.equalTo(superview.snp.edges).inset(inset) } self.configure(by: viewModel) self.configureLayout(by: viewModel) } ...
  • 31. func configure(by viewModel: CardViewModelType) { self.disposeBag = viewModel.disposeBag self.coverImageView?.setImage( with: viewModel.coverImageURLString, transformation: viewModel.cardType.coverImageSize.cloudinaryTransformation ) ... self.favoriteButton?.removeTarget(self, action: nil, for: .allEvents) if let favoriteButton = self.favoriteButton { let needUserName = User.notification .map { $0.name?.isEmpty ?? true } let tapFollowButton = favoriteButton.rx.tap.asDriver() .withLatestFrom(needUserName) { $1 } .flatMap { needUserName -> Driver<Bool> in guard needUserName else { return Driver.just(false) } return User.noNameAlert.rx.alert().asDriver(onErrorJustReturn: false) } tapFollowButton .filter { $0 }.map { _ in } .drive(ProfileViewController.present) .addDisposableTo(self.disposeBag) tapFollowButton .filter { !$0 } .withLatestFrom(viewModel.wish!) { $1 } .drive(viewModel.didWish!) .addDisposableTo(self.disposeBag) viewModel.wish? .drive(favoriteButton.rx.isSelected) .addDisposableTo(self.disposeBag) } } ...
  • 32. ... private func configureLayout(by viewModel: CardViewModelType) { switch viewModel.cardType { case .big: self.configureLayoutForBig(by: viewModel) case .small: self.configureLayoutForSmall(by: viewModel) case .realFinalISwearGodFinalType: self.configureLayoutForFinal(by: viewModel) } } private func configureLayoutForFinal(by viewModel: CardViewModelType) { ... } private func configureLayoutForBig(by viewModel: CardViewModelType) { ... } private func configureLayoutForSmall(by viewModel: CardViewModelType) { ... } }
  • 33. final class CardButton: UIView, CardViewContainerType { var cardView: CardViewType? { return self.subviews.first as? CardViewType } func configure(with cardViewModel: CardViewModelType) { guard let cardView = self.cardView else { let cardView = CardView(on: self, with: cardViewModel) cardView.isUserInteractionEnabled = false return } cardView.configure(by: cardViewModel) } required init(cardViewModel: CardViewModelType) { super.init(frame: cardViewModel.cardType.cellSize.rect)) self.configure(with: cardViewModel) } }
  • 34. let viewModel = CardViewModel(cardType: .realFinalISwearGodFinalType, data: $0) let button = CardButton(cardViewModel: viewModel) self.stackView.addArrangedSubview(button)