Weitere ähnliche Inhalte Ähnlich wie 기획, 디자인 변경에 강한 카드뷰 만들기 - iOS Tech Talk 2017 (20) 기획, 디자인 변경에 강한 카드뷰 만들기 - iOS Tech Talk 201710. 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
}
}
}
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) {
...
}
}
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
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
}
}
...
}
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)