Skip to content

Commit

Permalink
💲[Native Checkout] Pledge amount Stepper and Textfield input + Done b…
Browse files Browse the repository at this point in the history
…utton (#719)

* Add inputAccessoryView to amount text field

* Expose done button getter

* Dismiss text field by tapping done button

* Dismiss text field by tapping anywhere else

* Bind stepper and text field changes and properly enable/disable done button

* Batch layout constraints activation

* Make textfield signal point free
  • Loading branch information
dusi committed Jun 25, 2019
1 parent 7d74c02 commit 73ad884
Show file tree
Hide file tree
Showing 6 changed files with 248 additions and 16 deletions.
44 changes: 43 additions & 1 deletion Kickstarter-iOS/Views/AmountInputView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,39 @@ import Library
import Prelude
import UIKit

private enum Layout {
enum Button {
static let height: CGFloat = 34
}

enum Toolbar {
static let height: CGFloat = 54
}
}

class AmountInputView: UIView {
// MARK: - Properties

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

private(set) lazy var label: UILabel = { UILabel(frame: .zero) }()
private(set) lazy var textField: UITextField = { UITextField(frame: .zero) }()
private(set) lazy var textField: UITextField = {
UITextField(frame: .zero)
|> \.inputAccessoryView .~ self.toolbar
}()

private lazy var toolbar: UIToolbar = {
UIToolbar(frame: .zero)
|> \.items .~ [
UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil),
UIBarButtonItem(customView: self.doneButton)
]
|> \.translatesAutoresizingMaskIntoConstraints .~ false
}()

private lazy var stackView: UIStackView = { UIStackView(frame: .zero) }()
private var labelCenterYAnchor: NSLayoutConstraint?

Expand All @@ -21,6 +49,13 @@ class AmountInputView: UIView {

_ = ([self.label, self.textField], self.stackView)
|> ksr_addArrangedSubviewsToStackView()

self.toolbar.sizeToFit()

NSLayoutConstraint.activate([
self.doneButton.heightAnchor.constraint(equalToConstant: Layout.Button.height),
self.toolbar.heightAnchor.constraint(equalToConstant: Layout.Toolbar.height)
])
}

required init?(coder _: NSCoder) {
Expand All @@ -36,12 +71,19 @@ class AmountInputView: UIView {
|> checkoutWhiteBackgroundStyle
|> checkoutRoundedCornersStyle

_ = self.doneButton
|> keyboardDoneButtonStyle
|> UIButton.lens.title(for: .normal) %~ { _ in Strings.Done() }

_ = self.label
|> labelStyle

_ = self.textField
|> textFieldStyle

_ = self.toolbar
|> keyboardToolbarStyle

let isAccessibilityCategory = self.traitCollection.preferredContentSizeCategory.isAccessibilityCategory

_ = self.stackView
Expand Down
24 changes: 23 additions & 1 deletion Kickstarter-iOS/Views/Cells/PledgeAmountCell.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,18 @@ final class PledgeAmountCell: UITableViewCell, ValueCell {

self.spacer.widthAnchor.constraint(greaterThanOrEqualToConstant: Styles.grid(3)).isActive = true

self.amountInputView.doneButton.addTarget(
self,
action: #selector(PledgeAmountCell.doneButtonTapped(_:)),
for: .touchUpInside
)

self.amountInputView.textField.addTarget(
self,
action: #selector(PledgeAmountCell.textFieldDidChange(_:)),
for: .editingChanged
)

self.stepper.addTarget(
self,
action: #selector(PledgeAmountCell.stepperValueChanged(_:)),
Expand Down Expand Up @@ -84,11 +96,13 @@ final class PledgeAmountCell: UITableViewCell, ValueCell {
override func bindViewModel() {
super.bindViewModel()

self.amountInputView.doneButton.rac.enabled = self.viewModel.outputs.doneButtonIsEnabled
self.amountInputView.label.rac.text = self.viewModel.outputs.currency
self.amountInputView.textField.rac.isFirstResponder = self.viewModel.outputs.textFieldIsFirstResponder
self.amountInputView.textField.rac.text = self.viewModel.outputs.amount
self.stepper.rac.maximumValue = self.viewModel.outputs.stepperMaxValue
self.stepper.rac.minimumValue = self.viewModel.outputs.stepperMinValue
self.stepper.rac.value = self.viewModel.outputs.stepperInitialValue
self.stepper.rac.value = self.viewModel.outputs.stepperValue

self.viewModel.outputs.generateSelectionFeedback
.observeForUI()
Expand All @@ -107,9 +121,17 @@ final class PledgeAmountCell: UITableViewCell, ValueCell {

// MARK: - Actions

@objc func doneButtonTapped(_: UIButton) {
self.viewModel.inputs.doneButtonTapped()
}

@objc func stepperValueChanged(_ stepper: UIStepper) {
self.viewModel.inputs.stepperValueChanged(stepper.value)
}

@objc func textFieldDidChange(_ textField: UITextField) {
self.viewModel.inputs.textFieldValueChanged(textField.text)
}
}

// MARK: - Styles
Expand Down
11 changes: 11 additions & 0 deletions Kickstarter-iOS/Views/Controllers/PledgeTableViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ class PledgeTableViewController: UITableViewController {
self.tableView.registerCellClass(PledgeShippingLocationCell.self)
self.tableView.registerHeaderFooterClass(PledgeFooterView.self)

// Rebase Rebase Rebase
self.tableView.addGestureRecognizer(
UITapGestureRecognizer(target: self, action: #selector(PledgeTableViewController.dismissKeyboard))
)

self.viewModel.inputs.viewDidLoad()
}

Expand All @@ -58,6 +63,12 @@ class PledgeTableViewController: UITableViewController {
}
}

// MARK: - Actions

@objc func dismissKeyboard() {
self.tableView.endEditing(true)
}

// MARK: - UITableViewDelegate

override func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
Expand Down
17 changes: 17 additions & 0 deletions Library/Styles/BaseStyles.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public typealias SwitchControlStyle = (UISwitch) -> UISwitch
public typealias TableViewStyle = (UITableView) -> UITableView
public typealias TextFieldStyle = (UITextField) -> UITextField
public typealias TextViewStyle = (UITextView) -> UITextView
public typealias ToolbarStyle = (UIToolbar) -> UIToolbar
public typealias ViewStyle = (UIView) -> UIView

public func baseControllerStyle<VC: UIViewControllerProtocol>() -> ((VC) -> VC) {
Expand Down Expand Up @@ -149,3 +150,19 @@ private let baseNavigationBarStyle =
<> UINavigationBar.lens.isTranslucent .~ false
<> UINavigationBar.lens.barTintColor .~ .white
<> UINavigationBar.lens.tintColor .~ .ksr_green_700

public let keyboardToolbarStyle: ToolbarStyle = { toolbar -> UIToolbar in
toolbar
|> roundedStyle(cornerRadius: 8)
|> \.layer.backgroundColor .~ UIColor.white.cgColor
|> \.layer.maskedCorners .~ [.layerMaxXMinYCorner, .layerMinXMinYCorner]
}

public let keyboardDoneButtonStyle: ButtonStyle = { button -> UIButton in
button
|> greenButtonStyle
|> roundedStyle(cornerRadius: 6)
|> UIButton.lens.contentEdgeInsets .~ UIEdgeInsets(topBottom: Styles.grid(1), leftRight: Styles.grid(2))
|> UIButton.lens.layer.borderWidth .~ 0
|> UIButton.lens.titleLabel.font .~ UIFont.boldSystemFont(ofSize: 17) // Disables Dynamic Type support
}
58 changes: 52 additions & 6 deletions Library/ViewModels/PledgeAmountCellViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,21 @@ import ReactiveSwift

public protocol PledgeAmountCellViewModelInputs {
func configureWith(project: Project, reward: Reward)
func doneButtonTapped()
func stepperValueChanged(_ value: Double)
func textFieldValueChanged(_ value: String?)
}

public protocol PledgeAmountCellViewModelOutputs {
var amount: Signal<String, Never> { get }
var currency: Signal<String, Never> { get }
var doneButtonIsEnabled: Signal<Bool, Never> { get }
var generateSelectionFeedback: Signal<Void, Never> { get }
var generateNotificationWarningFeedback: Signal<Void, Never> { get }
var stepperInitialValue: Signal<Double, Never> { get }
var stepperMaxValue: Signal<Double, Never> { get }
var stepperMinValue: Signal<Double, Never> { get }
var stepperValue: Signal<Double, Never> { get }
var textFieldIsFirstResponder: Signal<Bool, Never> { get }
}

public protocol PledgeAmountCellViewModelType {
Expand All @@ -38,13 +42,13 @@ public final class PledgeAmountCellViewModel: PledgeAmountCellViewModelType,
let initialValue = Signal.combineLatest(project, reward)
.map { _ in 15.0 }

self.stepperInitialValue = initialValue

self.amount = Signal.merge(
let stepperValue = Signal.merge(
initialValue,
self.stepperValueProperty.signal
)
.map { String(format: "%.0f", $0) }

self.amount = stepperValue
.map { String(format: "%.0f", $0) }

self.currency = project
.map { currencySymbol(forCountry: $0.country).trimmed() }
Expand All @@ -71,25 +75,67 @@ public final class PledgeAmountCellViewModel: PledgeAmountCellViewModelType,
self.generateNotificationWarningFeedback = stepperValueChanged
.filter { min, max, value in value <= min || max <= value }
.ignoreValues()

let textFieldValue = self.textFieldValueProperty.signal
.skipNil()
.map(Double.init)
.skipNil()

self.doneButtonIsEnabled = Signal.combineLatest(
self.stepperMinValue,
self.stepperMaxValue,
Signal.merge(
stepperValue,
textFieldValue.signal
)
)
.map { min, max, doubleValue in min <= doubleValue && doubleValue <= max }

let clampedStepperValue = Signal.combineLatest(
self.stepperMinValue,
self.stepperMaxValue,
textFieldValue.signal
)
.map { minValue, maxValue, value in min(max(minValue, value), maxValue) }

self.stepperValue = Signal.merge(
initialValue,
clampedStepperValue
)

self.textFieldIsFirstResponder = self.doneButtonTappedProperty.signal
.mapConst(false)
}

private let projectAndRewardProperty = MutableProperty<(Project, Reward)?>(nil)
public func configureWith(project: Project, reward: Reward) {
self.projectAndRewardProperty.value = (project, reward)
}

private let doneButtonTappedProperty = MutableProperty(())
public func doneButtonTapped() {
self.doneButtonTappedProperty.value = ()
}

private let stepperValueProperty = MutableProperty<Double>(0)
public func stepperValueChanged(_ value: Double) {
self.stepperValueProperty.value = value
}

private let textFieldValueProperty = MutableProperty<String?>(nil)
public func textFieldValueChanged(_ value: String?) {
self.textFieldValueProperty.value = value
}

public let amount: Signal<String, Never>
public let currency: Signal<String, Never>
public let doneButtonIsEnabled: Signal<Bool, Never>
public let generateSelectionFeedback: Signal<Void, Never>
public let generateNotificationWarningFeedback: Signal<Void, Never>
public let stepperInitialValue: Signal<Double, Never>
public let stepperMaxValue: Signal<Double, Never>
public let stepperMinValue: Signal<Double, Never>
public let stepperValue: Signal<Double, Never>
public let textFieldIsFirstResponder: Signal<Bool, Never>

public var inputs: PledgeAmountCellViewModelInputs { return self }
public var outputs: PledgeAmountCellViewModelOutputs { return self }
Expand Down
Loading

0 comments on commit 73ad884

Please sign in to comment.