SlideShare une entreprise Scribd logo
1  sur  35
Télécharger pour lire hors ligne
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

Contenu connexe

Tendances

Design strategies for AngularJS
Design strategies for AngularJSDesign strategies for AngularJS
Design strategies for AngularJSSmartOrg
 
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 2018Husam Aamer
 
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.v1Bitla Software
 
Jquery In Rails
Jquery In RailsJquery In Rails
Jquery In Railsshen liu
 

Tendances (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
 

En vedette

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 systemCluster TWEED
 
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 SourcingJohn William
 
Maaseuturahaston hankekoulutus 14.1.2016, maksatusosio
Maaseuturahaston hankekoulutus 14.1.2016, maksatusosioMaaseuturahaston hankekoulutus 14.1.2016, maksatusosio
Maaseuturahaston hankekoulutus 14.1.2016, maksatusosioPohjois-Pohjanmaan ELY-keskus
 
Viene el rio recitando power point1
Viene el rio recitando power point1Viene el rio recitando power point1
Viene el rio recitando power point1musimusikera1
 
Periodontal disease
Periodontal diseasePeriodontal disease
Periodontal diseasevmuf
 
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 YeriAyak ve Bilek Cerrahisi
 
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...Askozia
 
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éricosazrahim
 
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, EnglishAskozia
 

En vedette (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
 

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

[22]Efficient and Testable MVVM pattern
[22]Efficient and Testable MVVM pattern[22]Efficient and Testable MVVM pattern
[22]Efficient and Testable MVVM patternNAVER Engineering
 
Jarv.us Showcase — SenchaCon 2011
Jarv.us Showcase — SenchaCon 2011Jarv.us Showcase — SenchaCon 2011
Jarv.us Showcase — SenchaCon 2011Chris Alfano
 
“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 AirAvitoTech
 
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 2019UA Mobile
 
前端MVC 豆瓣说
前端MVC 豆瓣说前端MVC 豆瓣说
前端MVC 豆瓣说Ting Lv
 
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.pdfShaiAlmog1
 
Developing Google Glass
Developing Google GlassDeveloping Google Glass
Developing Google GlassJohnny Sung
 
Th 0230 turbo_chargeyourui-howtomakeyourandroidu_ifastandefficient
Th 0230 turbo_chargeyourui-howtomakeyourandroidu_ifastandefficientTh 0230 turbo_chargeyourui-howtomakeyourandroidu_ifastandefficient
Th 0230 turbo_chargeyourui-howtomakeyourandroidu_ifastandefficientBin Shao
 
Arquitetando seu aplicativo Android com Jetpack
Arquitetando seu aplicativo Android com JetpackArquitetando seu aplicativo Android com Jetpack
Arquitetando seu aplicativo Android com JetpackNelson Glauber Leal
 
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 2022Andrzej Jóźwiak
 
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...Sencha
 
Practical Model View Programming (Roadshow Version)
Practical Model View Programming (Roadshow Version)Practical Model View Programming (Roadshow Version)
Practical Model View Programming (Roadshow Version)Marius Bugge Monsen
 
Getting Started with Combine And SwiftUI
Getting Started with Combine And SwiftUIGetting Started with Combine And SwiftUI
Getting Started with Combine And SwiftUIScott Gardner
 
PWA night vol.11 20191218
PWA night vol.11 20191218PWA night vol.11 20191218
PWA night vol.11 20191218bitpart
 

Similaire à 기획, 디자인 변경에 강한 카드뷰 만들기 - 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
 

Plus de Wanbok Choi

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

Plus 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 안드로이드 응용프로그램의 구조
 

Dernier

BDSM⚡Call Girls in Sector 71 Noida Escorts >༒8448380779 Escort Service
BDSM⚡Call Girls in Sector 71 Noida Escorts >༒8448380779 Escort ServiceBDSM⚡Call Girls in Sector 71 Noida Escorts >༒8448380779 Escort Service
BDSM⚡Call Girls in Sector 71 Noida Escorts >༒8448380779 Escort ServiceDelhi Call girls
 
CALL ON ➥8923113531 🔝Call Girls Gomti Nagar Lucknow best Night Fun service
CALL ON ➥8923113531 🔝Call Girls Gomti Nagar Lucknow best Night Fun serviceCALL ON ➥8923113531 🔝Call Girls Gomti Nagar Lucknow best Night Fun service
CALL ON ➥8923113531 🔝Call Girls Gomti Nagar Lucknow best Night Fun serviceanilsa9823
 
FULL ENJOY - 9999218229 Call Girls in {Mahipalpur}| Delhi NCR
FULL ENJOY - 9999218229 Call Girls in {Mahipalpur}| Delhi NCRFULL ENJOY - 9999218229 Call Girls in {Mahipalpur}| Delhi NCR
FULL ENJOY - 9999218229 Call Girls in {Mahipalpur}| Delhi NCRnishacall1
 
CALL ON ➥8923113531 🔝Call Girls Saharaganj Lucknow best sexual service
CALL ON ➥8923113531 🔝Call Girls Saharaganj Lucknow best sexual serviceCALL ON ➥8923113531 🔝Call Girls Saharaganj Lucknow best sexual service
CALL ON ➥8923113531 🔝Call Girls Saharaganj Lucknow best sexual serviceanilsa9823
 
哪里有卖的《俄亥俄大学学历证书+俄亥俄大学文凭证书+俄亥俄大学学位证书》Q微信741003700《俄亥俄大学学位证书复制》办理俄亥俄大学毕业证成绩单|购买...
哪里有卖的《俄亥俄大学学历证书+俄亥俄大学文凭证书+俄亥俄大学学位证书》Q微信741003700《俄亥俄大学学位证书复制》办理俄亥俄大学毕业证成绩单|购买...哪里有卖的《俄亥俄大学学历证书+俄亥俄大学文凭证书+俄亥俄大学学位证书》Q微信741003700《俄亥俄大学学位证书复制》办理俄亥俄大学毕业证成绩单|购买...
哪里有卖的《俄亥俄大学学历证书+俄亥俄大学文凭证书+俄亥俄大学学位证书》Q微信741003700《俄亥俄大学学位证书复制》办理俄亥俄大学毕业证成绩单|购买...wyqazy
 
Call US Pooja 9892124323 ✓Call Girls In Mira Road ( Mumbai ) secure service,
Call US Pooja 9892124323 ✓Call Girls In Mira Road ( Mumbai ) secure service,Call US Pooja 9892124323 ✓Call Girls In Mira Road ( Mumbai ) secure service,
Call US Pooja 9892124323 ✓Call Girls In Mira Road ( Mumbai ) secure service,Pooja Nehwal
 
Powerful Love Spells in Arkansas, AR (310) 882-6330 Bring Back Lost Lover
Powerful Love Spells in Arkansas, AR (310) 882-6330 Bring Back Lost LoverPowerful Love Spells in Arkansas, AR (310) 882-6330 Bring Back Lost Lover
Powerful Love Spells in Arkansas, AR (310) 882-6330 Bring Back Lost LoverPsychicRuben LoveSpells
 
9892124323 | Book Call Girls in Juhu and escort services 24x7
9892124323 | Book Call Girls in Juhu and escort services 24x79892124323 | Book Call Girls in Juhu and escort services 24x7
9892124323 | Book Call Girls in Juhu and escort services 24x7Pooja Nehwal
 
Chandigarh Call Girls Service ❤️🍑 9115573837 👄🫦Independent Escort Service Cha...
Chandigarh Call Girls Service ❤️🍑 9115573837 👄🫦Independent Escort Service Cha...Chandigarh Call Girls Service ❤️🍑 9115573837 👄🫦Independent Escort Service Cha...
Chandigarh Call Girls Service ❤️🍑 9115573837 👄🫦Independent Escort Service Cha...Niamh verma
 

Dernier (9)

BDSM⚡Call Girls in Sector 71 Noida Escorts >༒8448380779 Escort Service
BDSM⚡Call Girls in Sector 71 Noida Escorts >༒8448380779 Escort ServiceBDSM⚡Call Girls in Sector 71 Noida Escorts >༒8448380779 Escort Service
BDSM⚡Call Girls in Sector 71 Noida Escorts >༒8448380779 Escort Service
 
CALL ON ➥8923113531 🔝Call Girls Gomti Nagar Lucknow best Night Fun service
CALL ON ➥8923113531 🔝Call Girls Gomti Nagar Lucknow best Night Fun serviceCALL ON ➥8923113531 🔝Call Girls Gomti Nagar Lucknow best Night Fun service
CALL ON ➥8923113531 🔝Call Girls Gomti Nagar Lucknow best Night Fun service
 
FULL ENJOY - 9999218229 Call Girls in {Mahipalpur}| Delhi NCR
FULL ENJOY - 9999218229 Call Girls in {Mahipalpur}| Delhi NCRFULL ENJOY - 9999218229 Call Girls in {Mahipalpur}| Delhi NCR
FULL ENJOY - 9999218229 Call Girls in {Mahipalpur}| Delhi NCR
 
CALL ON ➥8923113531 🔝Call Girls Saharaganj Lucknow best sexual service
CALL ON ➥8923113531 🔝Call Girls Saharaganj Lucknow best sexual serviceCALL ON ➥8923113531 🔝Call Girls Saharaganj Lucknow best sexual service
CALL ON ➥8923113531 🔝Call Girls Saharaganj Lucknow best sexual service
 
哪里有卖的《俄亥俄大学学历证书+俄亥俄大学文凭证书+俄亥俄大学学位证书》Q微信741003700《俄亥俄大学学位证书复制》办理俄亥俄大学毕业证成绩单|购买...
哪里有卖的《俄亥俄大学学历证书+俄亥俄大学文凭证书+俄亥俄大学学位证书》Q微信741003700《俄亥俄大学学位证书复制》办理俄亥俄大学毕业证成绩单|购买...哪里有卖的《俄亥俄大学学历证书+俄亥俄大学文凭证书+俄亥俄大学学位证书》Q微信741003700《俄亥俄大学学位证书复制》办理俄亥俄大学毕业证成绩单|购买...
哪里有卖的《俄亥俄大学学历证书+俄亥俄大学文凭证书+俄亥俄大学学位证书》Q微信741003700《俄亥俄大学学位证书复制》办理俄亥俄大学毕业证成绩单|购买...
 
Call US Pooja 9892124323 ✓Call Girls In Mira Road ( Mumbai ) secure service,
Call US Pooja 9892124323 ✓Call Girls In Mira Road ( Mumbai ) secure service,Call US Pooja 9892124323 ✓Call Girls In Mira Road ( Mumbai ) secure service,
Call US Pooja 9892124323 ✓Call Girls In Mira Road ( Mumbai ) secure service,
 
Powerful Love Spells in Arkansas, AR (310) 882-6330 Bring Back Lost Lover
Powerful Love Spells in Arkansas, AR (310) 882-6330 Bring Back Lost LoverPowerful Love Spells in Arkansas, AR (310) 882-6330 Bring Back Lost Lover
Powerful Love Spells in Arkansas, AR (310) 882-6330 Bring Back Lost Lover
 
9892124323 | Book Call Girls in Juhu and escort services 24x7
9892124323 | Book Call Girls in Juhu and escort services 24x79892124323 | Book Call Girls in Juhu and escort services 24x7
9892124323 | Book Call Girls in Juhu and escort services 24x7
 
Chandigarh Call Girls Service ❤️🍑 9115573837 👄🫦Independent Escort Service Cha...
Chandigarh Call Girls Service ❤️🍑 9115573837 👄🫦Independent Escort Service Cha...Chandigarh Call Girls Service ❤️🍑 9115573837 👄🫦Independent Escort Service Cha...
Chandigarh Call Girls Service ❤️🍑 9115573837 👄🫦Independent Escort Service Cha...
 

기획, 디자인 변경에 강한 카드뷰 만들기 - 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)