Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

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

173 views

Published on

기획, 디자인 변경에 강한 카드뷰 만들기 - iOS Tech Talk 2017
2017년 2월 16일 쿠팡에서 발표.

Published in: Mobile
  • Be the first to comment

  • Be the first to like this

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

  1. 1. class CardTableViewCell: UITableViewCell
  2. 2. class ProductCardView: UIViewclass CardTableViewCell: UITableViewCell
  3. 3. 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) } } }
  4. 4. 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 } }() ...
  5. 5. ... 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 } } }
  6. 6. 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 } } ... }
  7. 7. 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) } ...
  8. 8. 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) } } ...
  9. 9. ... 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) { ... } }
  10. 10. 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) } }
  11. 11. 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) } }
  12. 12. 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
  13. 13. 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) } } }
  14. 14. 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) } } }
  15. 15. 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 } }
  16. 16. 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 } } ... }
  17. 17. 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) } ...
  18. 18. 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) } } ...
  19. 19. ... 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) { ... } }
  20. 20. 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) } }
  21. 21. let viewModel = CardViewModel(cardType: .realFinalISwearGodFinalType, data: $0) let button = CardButton(cardViewModel: viewModel) self.stackView.addArrangedSubview(button)

×