Skip to content

Commit

Permalink
💲[Native Checkout] Prepare reward pill collection view UI (#757)
Browse files Browse the repository at this point in the history
* Extract classNameWithoutModule to shared functions

* Update collection view convenience functions

* Update table view convenience functions

* Add pill collection view, cell, layout and data source

* Style pill cell

* Add pill collection to reward cell

* Format code

* Update font

* Fix a bug caused by a merge gone wrong

* Make adding / removing pill collection view controller more robust

* Refactor pill collection view

* Use consistent way to register collection view cell

* Remove nilling out delegate
  • Loading branch information
dusi authored and justinswart committed Aug 6, 2019
1 parent 2daf162 commit c9bf1fd
Show file tree
Hide file tree
Showing 9 changed files with 311 additions and 50 deletions.
22 changes: 22 additions & 0 deletions Kickstarter-iOS/DataSources/PillCollectionViewDataSource.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import Foundation
import Library
import UIKit

final class PillCollectionViewDataSource: ValueCellDataSource {
func load(_ values: [String]) {
self.set(
values: values,
cellClass: PillCell.self,
inSection: 0
)
}

override func configureCell(collectionCell cell: UICollectionViewCell, withValue value: Any) {
switch (cell, value) {
case let (cell as PillCell, value as String):
cell.configureWith(value: value)
default:
assertionFailure("Unrecognized (cell, value) combo.")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
@testable import Kickstarter_Framework
import Prelude
import XCTest

final class PillCollectionViewDataSourceTests: XCTestCase {
private let dataSource = PillCollectionViewDataSource()
private let collectionView = UICollectionView(
frame: .zero,
collectionViewLayout: UICollectionViewFlowLayout()
)

func testLoadValues() {
self.dataSource.load(["one", "two", "three"])

XCTAssertEqual(1, self.dataSource.numberOfSections(in: self.collectionView))
XCTAssertEqual(3, self.dataSource.collectionView(self.collectionView, numberOfItemsInSection: 0))
}
}
63 changes: 63 additions & 0 deletions Kickstarter-iOS/Views/Cells/PillCell.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import Library
import Prelude
import UIKit

final class PillCell: UICollectionViewCell, ValueCell {
// MARK: - Properties

private(set) lazy var label = {
UILabel(frame: .zero)
|> \.translatesAutoresizingMaskIntoConstraints .~ false
}()

// MARK: - Lifecycle

override init(frame: CGRect) {
super.init(frame: frame)

_ = self.contentView
|> \.layoutMargins .~ UIEdgeInsets(topBottom: Styles.gridHalf(2), leftRight: Styles.gridHalf(3))

_ = (self.label, self.contentView)
|> ksr_addSubviewToParent()
|> ksr_constrainViewToMarginsInParent()
}

required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

// MARK: - Styles

override func bindStyles() {
super.bindStyles()

_ = self.contentView
|> contentViewStyle

_ = self.label
|> labelStyle
}

// MARK: - Configuration

func configureWith(value: String) {
_ = self.label
|> \.text .~ value
}
}

// MARK: - Styles

private let contentViewStyle: ViewStyle = { view in
view
|> checkoutRoundedCornersStyle
|> \.backgroundColor .~ UIColor.ksr_green_500.withAlphaComponent(0.06)
}

private let labelStyle: LabelStyle = { label in
label
|> \.font .~ UIFont.ksr_footnote().bolded
|> \.numberOfLines .~ 0
|> \.textColor .~ UIColor.ksr_green_500
}
123 changes: 101 additions & 22 deletions Kickstarter-iOS/Views/Cells/RewardCell.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ final class RewardCell: UICollectionViewCell, ValueCell {
// MARK: - Properties

weak var delegate: RewardCellDelegate?
private let pillDataSource = PillCollectionViewDataSource()
private let viewModel: RewardCellViewModelType = RewardCellViewModel()

private let baseStackView: UIStackView = {
Expand All @@ -23,7 +24,6 @@ final class RewardCell: UICollectionViewCell, ValueCell {
private let descriptionLabel = UILabel(frame: .zero)
private let descriptionStackView = UIStackView(frame: .zero)
private let descriptionTitleLabel = UILabel(frame: .zero)

private let includedItemsStackView = UIStackView(frame: .zero)
private let includedItemsTitleLabel = UILabel(frame: .zero)
private let minimumPriceConversionLabel = UILabel(frame: .zero)
Expand All @@ -33,6 +33,28 @@ final class RewardCell: UICollectionViewCell, ValueCell {
|> \.translatesAutoresizingMaskIntoConstraints .~ false
}()

private lazy var pillCollectionView: UICollectionView = {
UICollectionView(
frame: .zero,
collectionViewLayout: PillLayout(
minimumInteritemSpacing: Styles.grid(1),
minimumLineSpacing: Styles.grid(1),
sectionInset: UIEdgeInsets(topBottom: Styles.grid(1))
)
)
|> \.backgroundColor .~ UIColor.white
|> \.contentInsetAdjustmentBehavior .~ UIScrollView.ContentInsetAdjustmentBehavior.always
|> \.dataSource .~ self.pillDataSource
|> \.delegate .~ self
|> \.isHidden .~ true
|> \.translatesAutoresizingMaskIntoConstraints .~ false
}()

private lazy var pillCollectionViewHeightConstraint: NSLayoutConstraint = {
self.pillCollectionView.heightAnchor.constraint(equalToConstant: 0)
|> \.priority .~ .defaultHigh
}()

private let pledgeButtonLayoutGuide = UILayoutGuide()
private let priceStackView = UIStackView(frame: .zero)
private let rewardTitleLabel = UILabel(frame: .zero)
Expand All @@ -50,17 +72,40 @@ final class RewardCell: UICollectionViewCell, ValueCell {

private let titleStackView = UIStackView(frame: .zero)

// MARK: - Lifecycle

override init(frame: CGRect) {
super.init(frame: frame)

self.configureViews()
self.setupConstraints()
self.bindViewModel()

self.pledgeButton.addTarget(self, action: #selector(self.pledgeButtonTapped), for: .touchUpInside)
self.containerView.addGestureRecognizer(
UITapGestureRecognizer(target: self, action: #selector(self.rewardCardTapped))
)
}

required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func layoutSubviews() {
super.layoutSubviews()

self.updateCollectionViewConstraints()
}

override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)

self.updateCollectionViewConstraints()
self.pillCollectionView.reloadData()
}

// MARK: - Styles

override func bindStyles() {
super.bindStyles()

Expand Down Expand Up @@ -135,6 +180,8 @@ final class RewardCell: UICollectionViewCell, ValueCell {
|> titleStackViewStyle
}

// MARK: - View model

override func bindViewModel() {
super.bindViewModel()

Expand Down Expand Up @@ -166,6 +213,9 @@ final class RewardCell: UICollectionViewCell, ValueCell {
_ = self?.containerView
?|> \.isUserInteractionEnabled .~ isUserInteractionEnabled
}

self.pillDataSource.load(["Ends in 3 days", "16 left"])
self.pillCollectionView.reloadData()
}

// MARK: - Private Helpers
Expand Down Expand Up @@ -194,8 +244,13 @@ final class RewardCell: UICollectionViewCell, ValueCell {
_ = (self.pledgeButtonLayoutGuide, self.containerView)
|> ksr_addLayoutGuideToView()

let baseSubviews = [
self.titleStackView, self.rewardTitleLabel, self.includedItemsStackView, self.descriptionStackView
let baseSubviews: [UIView] = [
self.titleStackView,
self.priceStackView,
self.rewardTitleLabel,
self.includedItemsStackView,
self.descriptionStackView,
self.pillCollectionView
]

_ = (baseSubviews, self.baseStackView)
Expand All @@ -219,28 +274,26 @@ final class RewardCell: UICollectionViewCell, ValueCell {
_ = ([self.priceStackView, self.stateImageViewContainer], self.titleStackView)
|> ksr_addArrangedSubviewsToStackView()

self.setupConstraints()

self.pledgeButton.addTarget(self, action: #selector(self.pledgeButtonTapped), for: .touchUpInside)

let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.rewardCardTapped))
self.containerView.addGestureRecognizer(tapGestureRecognizer)
self.pillCollectionView.register(PillCell.self)
}

private func setupConstraints() {
// Margins
let containerMargins = self.containerView.layoutMarginsGuide
let contentMargins = self.contentView.layoutMarginsGuide

// Constraints
let containerConstraints = [
self.containerView.widthAnchor.constraint(equalTo: self.contentView.widthAnchor)
]

let containerMargins = self.containerView.layoutMarginsGuide

let baseStackViewConstraints = [
self.baseStackView.leftAnchor.constraint(equalTo: containerMargins.leftAnchor),
self.baseStackView.rightAnchor.constraint(equalTo: containerMargins.rightAnchor),
self.baseStackView.topAnchor.constraint(equalTo: containerMargins.topAnchor)
]

let imageViewContraints = [
let stateImageViewContraints = [
self.stateImageView.widthAnchor.constraint(equalToConstant: Styles.grid(3)),
self.stateImageView.heightAnchor.constraint(equalTo: self.stateImageView.widthAnchor),
self.stateImageView.centerXAnchor.constraint(equalTo: self.stateImageViewContainer.centerXAnchor),
Expand All @@ -249,14 +302,15 @@ final class RewardCell: UICollectionViewCell, ValueCell {
self.stateImageViewContainer.heightAnchor.constraint(equalTo: self.stateImageViewContainer.widthAnchor)
]

let topConstraint = self.pledgeButton.topAnchor
.constraint(equalTo: self.pledgeButtonLayoutGuide.topAnchor)
|> \.priority .~ .defaultLow

let contentMargins = self.contentView.layoutMarginsGuide
let pillCollectionViewConstraints = [
self.pillCollectionView.leftAnchor.constraint(equalTo: contentMargins.leftAnchor),
self.pillCollectionView.rightAnchor.constraint(equalTo: contentMargins.rightAnchor),
self.pillCollectionViewHeightConstraint
]

let pledgeButtonConstraints = [
topConstraint,
self.pledgeButton.topAnchor.constraint(equalTo: self.pledgeButtonLayoutGuide.topAnchor)
|> \.priority .~ .defaultLow,
self.pledgeButton.leftAnchor.constraint(equalTo: contentMargins.leftAnchor),
self.pledgeButton.rightAnchor.constraint(equalTo: contentMargins.rightAnchor),
self.pledgeButton.bottomAnchor.constraint(lessThanOrEqualTo: contentMargins.bottomAnchor),
Expand All @@ -267,20 +321,28 @@ final class RewardCell: UICollectionViewCell, ValueCell {
self.pledgeButtonLayoutGuide.bottomAnchor.constraint(equalTo: containerMargins.bottomAnchor),
self.pledgeButtonLayoutGuide.leftAnchor.constraint(equalTo: containerMargins.leftAnchor),
self.pledgeButtonLayoutGuide.rightAnchor.constraint(equalTo: containerMargins.rightAnchor),
// swiftlint:disable:next line_length
self.pledgeButtonLayoutGuide.topAnchor.constraint(equalTo: self.baseStackView.bottomAnchor, constant: Styles.grid(3)),
self.pledgeButtonLayoutGuide.topAnchor.constraint(
equalTo: self.baseStackView.bottomAnchor, constant: Styles.grid(3)
),
self.pledgeButtonLayoutGuide.heightAnchor.constraint(equalTo: pledgeButton.heightAnchor)
]

NSLayoutConstraint.activate([
containerConstraints,
baseStackViewConstraints,
imageViewContraints,
pillCollectionViewConstraints,
pledgeButtonConstraints,
pledgeButtonLayoutGuideConstraints
pledgeButtonLayoutGuideConstraints,
stateImageViewContraints
].flatMap { $0 })
}

private func updateCollectionViewConstraints() {
self.pillCollectionView.layoutIfNeeded()

self.pillCollectionViewHeightConstraint.constant = self.pillCollectionView.contentSize.height
}

fileprivate func load(items: [String]) {
_ = self.includedItemsStackView.subviews
||> { $0.removeFromSuperview() }
Expand Down Expand Up @@ -311,6 +373,8 @@ final class RewardCell: UICollectionViewCell, ValueCell {
|> ksr_addArrangedSubviewsToStackView()
}

// MARK: - Configuration

internal func configureWith(value: (Project, Either<Reward, Backing>)) {
self.viewModel.inputs.configureWith(project: value.0, rewardOrBacking: value.1)
}
Expand All @@ -326,6 +390,21 @@ final class RewardCell: UICollectionViewCell, ValueCell {
}
}

// MARK: - UICollectionViewDelegate

extension RewardCell: UICollectionViewDelegate {
func collectionView(
_ collectionView: UICollectionView,
willDisplay cell: UICollectionViewCell,
forItemAt _: IndexPath
) {
guard let pillCell = cell as? PillCell else { return }

_ = pillCell.label
|> \.preferredMaxLayoutWidth .~ collectionView.bounds.width
}
}

// MARK: - Styles

private let baseRewardLabelStyle: LabelStyle = { label in
Expand Down
Loading

0 comments on commit c9bf1fd

Please sign in to comment.