Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

πŸ’²[Native Checkout] Pledge amount Stepper and Textfield input + Done button #719

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can consider this here:

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

.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