Skip to content

Commit

Permalink
💲[Native Checkout] Stepper haptics (#711)
Browse files Browse the repository at this point in the history
* Refactor haptic feedback to being more testable

* Add haptic feedback for the amount stepper

* Update reactive extensions dependency

* Use UIStepper reactive extensions

* Reorder alphabetically

* Refactor feedback generators

* Fix capitalization typo
  • Loading branch information
dusi authored and justinswart committed Aug 6, 2019
1 parent 45d1933 commit 494246c
Show file tree
Hide file tree
Showing 20 changed files with 346 additions and 104 deletions.
2 changes: 1 addition & 1 deletion Cartfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
### Internal

github "kickstarter/Kickstarter-Prelude" "c96c90b026d45839724e827f881dcba3ec812725"
github "kickstarter/Kickstarter-ReactiveExtensions" "665b5cd4941e54e5d120b80218f62d6ae0e66dac"
github "kickstarter/Kickstarter-ReactiveExtensions" "1b12cd6236aaace5e90a75332309c15c3585e162"

### 3rd Party

Expand Down
2 changes: 1 addition & 1 deletion Cartfile.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ github "Alamofire/AlamofireImage" "c41f8b0acfbb3180fe045df73596e4332c338633"
github "ReactiveCocoa/ReactiveSwift" "6.0.0"
github "facebook/facebook-objc-sdk" "v5.0.2"
github "kickstarter/Kickstarter-Prelude" "c96c90b026d45839724e827f881dcba3ec812725"
github "kickstarter/Kickstarter-ReactiveExtensions" "665b5cd4941e54e5d120b80218f62d6ae0e66dac"
github "kickstarter/Kickstarter-ReactiveExtensions" "1b12cd6236aaace5e90a75332309c15c3585e162"
github "stripe/stripe-ios" "v13.2.0"
github "thoughtbot/Argo" "39f06f089d25c111444e5a85eef64586e54756ac"
github "thoughtbot/Curry" "b6bf27ec9d711f607a8c7da9ca69ee9eaa201a22"
Expand Down
32 changes: 32 additions & 0 deletions Kickstarter-iOS/Library/SharedFunctions.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,38 @@
import Library
import UIKit

// MARK: - Haptic feedback

func generateImpactFeedback(
feedbackGenerator: UIImpactFeedbackGeneratorType = UIImpactFeedbackGenerator(style: .light)
) {
feedbackGenerator.prepare()
feedbackGenerator.impactOccurred()
}

func generateNotificationSuccessFeedback(
feedbackGenerator: UINotificationFeedbackGeneratorType = UINotificationFeedbackGenerator()
) {
feedbackGenerator.prepare()
feedbackGenerator.notificationOccurred(.success)
}

func generateNotificationWarningFeedback(
feedbackGenerator: UINotificationFeedbackGeneratorType = UINotificationFeedbackGenerator()
) {
feedbackGenerator.prepare()
feedbackGenerator.notificationOccurred(.warning)
}

func generateSelectionFeedback(
feedbackGenerator: UISelectionFeedbackGeneratorType = UISelectionFeedbackGenerator()
) {
feedbackGenerator.prepare()
feedbackGenerator.selectionChanged()
}

// MARK: - Login workflow

public func logoutAndDismiss(
viewController: UIViewController,
appEnvironment: AppEnvironmentType.Type = AppEnvironment.self,
Expand Down
60 changes: 36 additions & 24 deletions Kickstarter-iOS/Library/SharedFunctionsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,42 @@
import XCTest

internal final class SharedFunctionsTests: XCTestCase {
func testGenerateImpactFeedback() {
let mockFeedbackGenerator = MockImpactFeedbackGenerator()

generateImpactFeedback(feedbackGenerator: mockFeedbackGenerator)

XCTAssertTrue(mockFeedbackGenerator.prepareWasCalled)
XCTAssertTrue(mockFeedbackGenerator.impactOccurredWasCalled)
}

func testGenerateNotificationSuccessFeedback() {
let mockFeedbackGenerator = MockNotificationFeedbackGenerator()

generateNotificationSuccessFeedback(feedbackGenerator: mockFeedbackGenerator)

XCTAssertTrue(mockFeedbackGenerator.prepareWasCalled)
XCTAssertTrue(mockFeedbackGenerator.notificationOccurredWasCalled)
}

func testGenerateNotificationWarningFeedback() {
let mockFeedbackGenerator = MockNotificationFeedbackGenerator()

generateNotificationWarningFeedback(feedbackGenerator: mockFeedbackGenerator)

XCTAssertTrue(mockFeedbackGenerator.prepareWasCalled)
XCTAssertTrue(mockFeedbackGenerator.notificationOccurredWasCalled)
}

func testGenerateSelectionFeedback() {
let mockFeedbackGenerator = MockSelectionFeedbackGenerator()

generateSelectionFeedback(feedbackGenerator: mockFeedbackGenerator)

XCTAssertTrue(mockFeedbackGenerator.prepareWasCalled)
XCTAssertTrue(mockFeedbackGenerator.selectionChangedWasCalled)
}

func testLogoutAndDismiss() {
let mockAppEnvironment = MockAppEnvironment.self
let mockPushNotificationDialog = MockPushNotificationDialog.self
Expand All @@ -20,27 +56,3 @@ internal final class SharedFunctionsTests: XCTestCase {
XCTAssertTrue(mockViewController.dismissAnimatedWasCalled)
}
}

private struct MockAppEnvironment: AppEnvironmentType {
static var logoutWasCalled = false

static func logout() {
self.logoutWasCalled = true
}
}

private struct MockPushNotificationDialog: PushNotificationDialogType {
static var resetAllContextsWasCalled = false

static func resetAllContexts() {
self.resetAllContextsWasCalled = true
}
}

private class MockViewController: UIViewController {
var dismissAnimatedWasCalled = false

override func dismiss(animated _: Bool, completion _: (() -> Void)? = nil) {
self.dismissAnimatedWasCalled = true
}
}
31 changes: 31 additions & 0 deletions Kickstarter-iOS/Library/UIFeedbackGeneratorType.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import UIKit

// MARK: - UIFeedbackGenerator

protocol UIFeedbackGeneratorType {
func prepare()
}

// MARK: - UIImpactFeedbackGeneratorType

protocol UIImpactFeedbackGeneratorType: UIFeedbackGeneratorType {
func impactOccurred()
}

extension UIImpactFeedbackGenerator: UIImpactFeedbackGeneratorType {}

// MARK: - UINotificationFeedbackGeneratorType

protocol UINotificationFeedbackGeneratorType: UIFeedbackGeneratorType {
func notificationOccurred(_ notificationType: UINotificationFeedbackGenerator.FeedbackType)
}

extension UINotificationFeedbackGenerator: UINotificationFeedbackGeneratorType {}

// MARK: - UISelectionFeedbackGeneratorType

protocol UISelectionFeedbackGeneratorType: UIFeedbackGeneratorType {
func selectionChanged()
}

extension UISelectionFeedbackGenerator: UISelectionFeedbackGeneratorType {}
27 changes: 27 additions & 0 deletions Kickstarter-iOS/TestHelpers/MockAppEnvironment.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import Foundation
@testable import Library
import UIKit

struct MockAppEnvironment: AppEnvironmentType {
static var logoutWasCalled = false

static func logout() {
self.logoutWasCalled = true
}
}

struct MockPushNotificationDialog: PushNotificationDialogType {
static var resetAllContextsWasCalled = false

static func resetAllContexts() {
self.resetAllContextsWasCalled = true
}
}

class MockViewController: UIViewController {
var dismissAnimatedWasCalled = false

override func dismiss(animated _: Bool, completion _: (() -> Void)? = nil) {
self.dismissAnimatedWasCalled = true
}
}
42 changes: 42 additions & 0 deletions Kickstarter-iOS/TestHelpers/MockFeedbackGenerator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import Foundation
@testable import Kickstarter_Framework
import UIKit

class MockImpactFeedbackGenerator: UIImpactFeedbackGeneratorType {
var prepareWasCalled = false
var impactOccurredWasCalled = false

func prepare() {
self.prepareWasCalled = true
}

func impactOccurred() {
self.impactOccurredWasCalled = true
}
}

class MockNotificationFeedbackGenerator: UINotificationFeedbackGeneratorType {
var prepareWasCalled = false
var notificationOccurredWasCalled = false

func prepare() {
self.prepareWasCalled = true
}

func notificationOccurred(_: UINotificationFeedbackGenerator.FeedbackType) {
self.notificationOccurredWasCalled = true
}
}

class MockSelectionFeedbackGenerator: UISelectionFeedbackGeneratorType {
var prepareWasCalled = false
var selectionChangedWasCalled = false

func prepare() {
self.prepareWasCalled = true
}

func selectionChanged() {
self.selectionChangedWasCalled = true
}
}
14 changes: 4 additions & 10 deletions Kickstarter-iOS/Views/Cells/DiscoveryPostcardCell.swift
Original file line number Diff line number Diff line change
Expand Up @@ -257,21 +257,15 @@ internal final class DiscoveryPostcardCell: UITableViewCell, ValueCell {

self.watchProjectViewModel.outputs.generateImpactFeedback
.observeForUI()
.observeValues { [weak self] in
self?.saveButton.generateImpactFeedback(style: .light)
}
.observeValues { _ in generateImpactFeedback() }

self.watchProjectViewModel.outputs.generateSuccessFeedback
self.watchProjectViewModel.outputs.generateNotificationSuccessFeedback
.observeForUI()
.observeValues { [weak self] in
self?.saveButton.generateSuccessFeedback()
}
.observeValues { generateNotificationSuccessFeedback() }

self.watchProjectViewModel.outputs.generateSelectionFeedback
.observeForUI()
.observeValues { [weak self] in
self?.saveButton.generateSelectionFeedback()
}
.observeValues { generateSelectionFeedback() }

self.viewModel.outputs.projectCategoryName
.signal
Expand Down
23 changes: 23 additions & 0 deletions Kickstarter-iOS/Views/Cells/PledgeAmountCell.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ final class PledgeAmountCell: UITableViewCell, ValueCell {

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

self.stepper.addTarget(
self,
action: #selector(PledgeAmountCell.stepperValueChanged(_:)),
for: .valueChanged
)

self.bindViewModel()
}

Expand Down Expand Up @@ -80,13 +86,30 @@ final class PledgeAmountCell: UITableViewCell, ValueCell {

self.amountInputView.label.rac.text = self.viewModel.outputs.currency
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.viewModel.outputs.generateSelectionFeedback
.observeForUI()
.observeValues { generateSelectionFeedback() }

self.viewModel.outputs.generateNotificationWarningFeedback
.observeForUI()
.observeValues { generateNotificationWarningFeedback() }
}

// MARK: - Configuration

func configureWith(value: (project: Project, reward: Reward)) {
self.viewModel.inputs.configureWith(project: value.project, reward: value.reward)
}

// MARK: - Actions

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

// MARK: - Styles
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ internal final class DeprecatedCheckoutViewController: DeprecatedWebViewControll
self.viewModel.outputs.goToThanks
.observeForControllerAction()
.observeValues { [weak self] project in
UIFeedbackGenerator.ksr_success()
generateNotificationSuccessFeedback()

self?.goToThanks(project: project)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +540,8 @@ internal final class DeprecatedRewardPledgeViewController: UIViewController {
self.viewModel.outputs.goToThanks
.observeForControllerAction()
.observeValues { [weak self] project in
UIFeedbackGenerator.ksr_success()
generateNotificationSuccessFeedback()

self?.goToThanks(project: project)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,21 +122,15 @@ public final class ProjectNavBarViewController: UIViewController {

self.watchProjectViewModel.outputs.generateImpactFeedback
.observeForUI()
.observeValues { [weak self] in
self?.saveButton.generateImpactFeedback(style: .light)
}
.observeValues { generateImpactFeedback() }

self.watchProjectViewModel.outputs.generateSuccessFeedback
self.watchProjectViewModel.outputs.generateNotificationSuccessFeedback
.observeForUI()
.observeValues { [weak self] in
self?.saveButton.generateSuccessFeedback()
}
.observeValues { generateNotificationSuccessFeedback() }

self.watchProjectViewModel.outputs.generateSelectionFeedback
.observeForUI()
.observeValues { [weak self] in
self?.saveButton.generateSelectionFeedback()
}
.observeValues { generateSelectionFeedback() }

self.watchProjectViewModel.outputs.showProjectSavedAlert
.observeForControllerAction()
Expand Down
Loading

0 comments on commit 494246c

Please sign in to comment.