diff --git a/Cartfile b/Cartfile index b3a8516870..2663b941ae 100644 --- a/Cartfile +++ b/Cartfile @@ -1,7 +1,7 @@ ### Internal github "kickstarter/Kickstarter-Prelude" "c96c90b026d45839724e827f881dcba3ec812725" -github "kickstarter/Kickstarter-ReactiveExtensions" "665b5cd4941e54e5d120b80218f62d6ae0e66dac" +github "kickstarter/Kickstarter-ReactiveExtensions" "1b12cd6236aaace5e90a75332309c15c3585e162" ### 3rd Party diff --git a/Cartfile.resolved b/Cartfile.resolved index 1b6700e6c9..6e8b4b497c 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -3,7 +3,7 @@ github "Alamofire/Alamofire" "5.0.0-beta.6" github "Alamofire/AlamofireImage" "c41f8b0acfbb3180fe045df73596e4332c338633" github "ReactiveCocoa/ReactiveSwift" "6.0.0" 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" diff --git a/Kickstarter-iOS/Library/SharedFunctions.swift b/Kickstarter-iOS/Library/SharedFunctions.swift index bb42da9b75..b9acf4a7ff 100644 --- a/Kickstarter-iOS/Library/SharedFunctions.swift +++ b/Kickstarter-iOS/Library/SharedFunctions.swift @@ -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, diff --git a/Kickstarter-iOS/Library/SharedFunctionsTests.swift b/Kickstarter-iOS/Library/SharedFunctionsTests.swift index e9fc6a1f25..a33a8d8842 100644 --- a/Kickstarter-iOS/Library/SharedFunctionsTests.swift +++ b/Kickstarter-iOS/Library/SharedFunctionsTests.swift @@ -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 @@ -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 - } -} diff --git a/Kickstarter-iOS/Library/UIFeedbackGeneratorType.swift b/Kickstarter-iOS/Library/UIFeedbackGeneratorType.swift new file mode 100644 index 0000000000..795394f087 --- /dev/null +++ b/Kickstarter-iOS/Library/UIFeedbackGeneratorType.swift @@ -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 {} diff --git a/Kickstarter-iOS/TestHelpers/MockAppEnvironment.swift b/Kickstarter-iOS/TestHelpers/MockAppEnvironment.swift new file mode 100644 index 0000000000..c7305c44b6 --- /dev/null +++ b/Kickstarter-iOS/TestHelpers/MockAppEnvironment.swift @@ -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 + } +} diff --git a/Kickstarter-iOS/TestHelpers/MockFeedbackGenerator.swift b/Kickstarter-iOS/TestHelpers/MockFeedbackGenerator.swift new file mode 100644 index 0000000000..93d24d47f5 --- /dev/null +++ b/Kickstarter-iOS/TestHelpers/MockFeedbackGenerator.swift @@ -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 + } +} diff --git a/Kickstarter-iOS/Views/Cells/DiscoveryPostcardCell.swift b/Kickstarter-iOS/Views/Cells/DiscoveryPostcardCell.swift index 3f8f989ec6..7149287099 100644 --- a/Kickstarter-iOS/Views/Cells/DiscoveryPostcardCell.swift +++ b/Kickstarter-iOS/Views/Cells/DiscoveryPostcardCell.swift @@ -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 diff --git a/Kickstarter-iOS/Views/Cells/PledgeAmountCell.swift b/Kickstarter-iOS/Views/Cells/PledgeAmountCell.swift index 8577e435d3..674cfac338 100644 --- a/Kickstarter-iOS/Views/Cells/PledgeAmountCell.swift +++ b/Kickstarter-iOS/Views/Cells/PledgeAmountCell.swift @@ -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() } @@ -80,6 +86,17 @@ 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 @@ -87,6 +104,12 @@ final class PledgeAmountCell: UITableViewCell, ValueCell { 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 diff --git a/Kickstarter-iOS/Views/Controllers/DeprecatedCheckoutViewController.swift b/Kickstarter-iOS/Views/Controllers/DeprecatedCheckoutViewController.swift index 4b8644381c..c6ed8bf4cb 100644 --- a/Kickstarter-iOS/Views/Controllers/DeprecatedCheckoutViewController.swift +++ b/Kickstarter-iOS/Views/Controllers/DeprecatedCheckoutViewController.swift @@ -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) } diff --git a/Kickstarter-iOS/Views/Controllers/DeprecatedRewardPledgeViewController.swift b/Kickstarter-iOS/Views/Controllers/DeprecatedRewardPledgeViewController.swift index 3408c16b35..1c39c8e166 100644 --- a/Kickstarter-iOS/Views/Controllers/DeprecatedRewardPledgeViewController.swift +++ b/Kickstarter-iOS/Views/Controllers/DeprecatedRewardPledgeViewController.swift @@ -539,7 +539,8 @@ internal final class DeprecatedRewardPledgeViewController: UIViewController { self.viewModel.outputs.goToThanks .observeForControllerAction() .observeValues { [weak self] project in - UIFeedbackGenerator.ksr_success() + generateNotificationSuccessFeedback() + self?.goToThanks(project: project) } diff --git a/Kickstarter-iOS/Views/Controllers/ProjectNavBarViewController.swift b/Kickstarter-iOS/Views/Controllers/ProjectNavBarViewController.swift index d6b093159f..b67eef6935 100644 --- a/Kickstarter-iOS/Views/Controllers/ProjectNavBarViewController.swift +++ b/Kickstarter-iOS/Views/Controllers/ProjectNavBarViewController.swift @@ -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() diff --git a/Kickstarter.xcodeproj/project.pbxproj b/Kickstarter.xcodeproj/project.pbxproj index 86f8fc9496..11f7c8f757 100644 --- a/Kickstarter.xcodeproj/project.pbxproj +++ b/Kickstarter.xcodeproj/project.pbxproj @@ -82,6 +82,9 @@ 3706409022A9BE9400889CBD /* DateFormatterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3706408F22A9BE9400889CBD /* DateFormatterTests.swift */; }; 3708DD43220A5A5700F8E569 /* SettingsGroupedFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3708DD42220A5A5700F8E569 /* SettingsGroupedFooterView.swift */; }; 3708DD45220A76FE00F8E569 /* CreatePasswordTableControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3708DD44220A76FD00F8E569 /* CreatePasswordTableControllerTests.swift */; }; + 37096C3022BC238C003D1F40 /* MockFeedbackGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37096C2F22BC238C003D1F40 /* MockFeedbackGenerator.swift */; }; + 37096C3422BC24DD003D1F40 /* UIFeedbackGeneratorType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37096C3322BC24DD003D1F40 /* UIFeedbackGeneratorType.swift */; }; + 37096C3522BC282E003D1F40 /* MockAppEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37096C3122BC23AD003D1F40 /* MockAppEnvironment.swift */; }; 370ACB00225D337900C8745F /* PledgeAmountCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 370ACAFF225D337900C8745F /* PledgeAmountCell.swift */; }; 370BE71622541C8100B44DB2 /* UIViewController+URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 370BE71522541C8100B44DB2 /* UIViewController+URL.swift */; }; 370F527A2254267900F159B9 /* UIApplicationType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 370F52792254267900F159B9 /* UIApplicationType.swift */; }; @@ -1065,8 +1068,6 @@ D775EC682084FDE800885634 /* ProjectStatsEnvelope.ReferralAggregateStatsLenses.swift in Sources */ = {isa = PBXBuildFile; fileRef = D775EC672084FDE800885634 /* ProjectStatsEnvelope.ReferralAggregateStatsLenses.swift */; }; D777442F217A3382008D679F /* ChangeCurrencyInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = D777442E217A3382008D679F /* ChangeCurrencyInput.swift */; }; D7774466217A345D008D679F /* UpdateUserProfileMutation .swift in Sources */ = {isa = PBXBuildFile; fileRef = D77743E2217A2D67008D679F /* UpdateUserProfileMutation .swift */; }; - D78E4E482188CB4300E99295 /* UIButton+HapticFeedback.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78E4E472188CB4300E99295 /* UIButton+HapticFeedback.swift */; }; - D78E4EE3218909DC00E99295 /* UIFeedbackGenerator+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78E4EE2218909DC00E99295 /* UIFeedbackGenerator+Extensions.swift */; }; D79440572203A63300D0A747 /* CreatePaymentSourceEnvelope.swift in Sources */ = {isa = PBXBuildFile; fileRef = D79440562203A63300D0A747 /* CreatePaymentSourceEnvelope.swift */; }; D79440902208970E00D0A747 /* CreatePaymentSourceTemplate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D794408F2208970E00D0A747 /* CreatePaymentSourceTemplate.swift */; }; D796867C20FE655300E54C61 /* SettingsFollowCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D796867B20FE655300E54C61 /* SettingsFollowCellViewModel.swift */; }; @@ -1323,6 +1324,9 @@ 3706408F22A9BE9400889CBD /* DateFormatterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateFormatterTests.swift; sourceTree = ""; }; 3708DD42220A5A5700F8E569 /* SettingsGroupedFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsGroupedFooterView.swift; sourceTree = ""; }; 3708DD44220A76FD00F8E569 /* CreatePasswordTableControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatePasswordTableControllerTests.swift; sourceTree = ""; }; + 37096C2F22BC238C003D1F40 /* MockFeedbackGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockFeedbackGenerator.swift; sourceTree = ""; }; + 37096C3122BC23AD003D1F40 /* MockAppEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAppEnvironment.swift; sourceTree = ""; }; + 37096C3322BC24DD003D1F40 /* UIFeedbackGeneratorType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIFeedbackGeneratorType.swift; sourceTree = ""; }; 370ACAFF225D337900C8745F /* PledgeAmountCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PledgeAmountCell.swift; sourceTree = ""; }; 370BE71522541C8100B44DB2 /* UIViewController+URL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+URL.swift"; sourceTree = ""; }; 370BE74E22541C8F00B44DB2 /* UIViewController+URLTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+URLTests.swift"; sourceTree = ""; }; @@ -2255,8 +2259,6 @@ D775EC672084FDE800885634 /* ProjectStatsEnvelope.ReferralAggregateStatsLenses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProjectStatsEnvelope.ReferralAggregateStatsLenses.swift; sourceTree = ""; }; D77743E2217A2D67008D679F /* UpdateUserProfileMutation .swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UpdateUserProfileMutation .swift"; sourceTree = ""; }; D777442E217A3382008D679F /* ChangeCurrencyInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChangeCurrencyInput.swift; sourceTree = ""; }; - D78E4E472188CB4300E99295 /* UIButton+HapticFeedback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIButton+HapticFeedback.swift"; sourceTree = ""; }; - D78E4EE2218909DC00E99295 /* UIFeedbackGenerator+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFeedbackGenerator+Extensions.swift"; sourceTree = ""; }; D79076F8207BC161008014EC /* CrossDissolveTransitionAnimator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrossDissolveTransitionAnimator.swift; sourceTree = ""; }; D79440562203A63300D0A747 /* CreatePaymentSourceEnvelope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatePaymentSourceEnvelope.swift; sourceTree = ""; }; D794408F2208970E00D0A747 /* CreatePaymentSourceTemplate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatePaymentSourceTemplate.swift; sourceTree = ""; }; @@ -2404,6 +2406,7 @@ D76436D1224040B700DAFC9E /* SharedFunctions.swift */, D764377A2241707D00DAFC9E /* SharedFunctionsTests.swift */, 379CFFEE2242DAC300F6F0C2 /* Storyboard.swift */, + 37096C3322BC24DD003D1F40 /* UIFeedbackGeneratorType.swift */, 379CFFEA2242DAC200F6F0C2 /* UIImageView+URL.swift */, 379CFFF12242DAC400F6F0C2 /* WebViewController.swift */, ); @@ -2904,12 +2907,10 @@ 370F52B2225426C700F159B9 /* UIApplication.swift */, 370F52792254267900F159B9 /* UIApplicationType.swift */, 0176E13A1C9742FD009CA092 /* UIBarButtonItem.swift */, - D78E4E472188CB4300E99295 /* UIButton+HapticFeedback.swift */, A7C725941C85D36D005A016B /* UIButton+LocalizedKey.swift */, 0151AE871C8F60370067F1BE /* UIColor.swift */, A7ED1F231E830FDC00BFFA01 /* UIColorTests.swift */, A78537BB1CB5416700385B73 /* UIDeviceType.swift */, - D78E4EE2218909DC00E99295 /* UIFeedbackGenerator+Extensions.swift */, 3757D0CD228E51F800241AE6 /* UIFont.swift */, 3757D106228E521600241AE6 /* UIFontTests.swift */, A7C725951C85D36D005A016B /* UIGestureRecognizer-Extensions.swift */, @@ -3118,6 +3119,8 @@ isa = PBXGroup; children = ( A7ED20211E83237F00BFFA01 /* Combos.swift */, + 37096C3122BC23AD003D1F40 /* MockAppEnvironment.swift */, + 37096C2F22BC238C003D1F40 /* MockFeedbackGenerator.swift */, A7ED20221E83237F00BFFA01 /* TraitController.swift */, ); path = TestHelpers; @@ -4241,7 +4244,6 @@ 9DC572E51D36CA9800AE209C /* ProjectActivityStyles.swift in Sources */, A78851911D6C900000617930 /* DeprecatedWebViewModel.swift in Sources */, A7F441C91D005A9400FE6FC5 /* MessageDialogViewModel.swift in Sources */, - D78E4E482188CB4300E99295 /* UIButton+HapticFeedback.swift in Sources */, A78537BC1CB5416700385B73 /* UIDeviceType.swift in Sources */, A7808BF01D625C6A001CF96A /* ProjectCreatorViewModel.swift in Sources */, A755115E1C8642C3005355CF /* Environment.swift in Sources */, @@ -4339,7 +4341,6 @@ 9D89B7E51D6B8DB90021F6FF /* WebModalViewModel.swift in Sources */, D0200A5721935F2D00F5CC27 /* GraphError+LocalizedDescription.swift in Sources */, D0A787BB2204D66B006AE4F4 /* SelectCurrencyViewModel.swift in Sources */, - D78E4EE3218909DC00E99295 /* UIFeedbackGenerator+Extensions.swift in Sources */, A75C81261D210F1F00B5AD03 /* ShareContext.swift in Sources */, A75511671C8642C3005355CF /* UIButton+LocalizedKey.swift in Sources */, D60C8BE92142D61A00D96152 /* SettingsAccountCellType.swift in Sources */, @@ -4672,6 +4673,7 @@ 379CFFFF2242DAF900F6F0C2 /* UIImageView+URL.swift in Sources */, 59AE35E21D67643100A310E6 /* DiscoveryPostcardCell.swift in Sources */, A7FA38A41D9068940041FC9C /* PledgeTitleCell.swift in Sources */, + 37096C3422BC24DD003D1F40 /* UIFeedbackGeneratorType.swift in Sources */, 59B0DFC51D11AC850081D2DC /* DashboardDataSource.swift in Sources */, A757EABD1D19FAEE00A5C978 /* ProjectActivitiesViewController.swift in Sources */, A745D0221CA897FF00C12802 /* SearchViewController.swift in Sources */, @@ -4875,10 +4877,12 @@ A7ED20651E83256700BFFA01 /* UpdatePreviewViewModelTests.swift in Sources */, 77891BDE20CEB6DB00B46D5D /* ThanksProjectsDataSourceTests.swift in Sources */, A7ED20571E8323E900BFFA01 /* ActivitiesViewControllerTests.swift in Sources */, + 37096C3522BC282E003D1F40 /* MockAppEnvironment.swift in Sources */, A7ED204A1E8323E900BFFA01 /* DeprecatedRewardPledgeViewControllerTests.swift in Sources */, A7ED20401E8323E900BFFA01 /* DiscoveryFiltersViewControllerTests.swift in Sources */, 778215EE20F7AB8300F3D09F /* HelpViewControllerTests.swift in Sources */, D6E7DAFC22089F9800689BD6 /* SettingsViewControllerTests.swift in Sources */, + 37096C3022BC238C003D1F40 /* MockFeedbackGenerator.swift in Sources */, D6E925CE211107CD00E13010 /* SettingsNewslettersDataSourceTests.swift in Sources */, 77FA6CD220F53E5E00809E31 /* SettingsDataSourceTests.swift in Sources */, A7ED204D1E8323E900BFFA01 /* FindFriendsViewControllerTests.swift in Sources */, diff --git a/Library/TestHelpers/XCTestCase+AppEnvironment.swift b/Library/TestHelpers/XCTestCase+AppEnvironment.swift index 0e58fe9427..40866750a7 100644 --- a/Library/TestHelpers/XCTestCase+AppEnvironment.swift +++ b/Library/TestHelpers/XCTestCase+AppEnvironment.swift @@ -4,6 +4,7 @@ import KsApi import ReactiveSwift import XCTest +// swiftlint:disable line_length extension XCTestCase { // Pushes an environment onto the stack, executes a closure, and then pops the environment from the stack. func withEnvironment(_ env: Environment, body: () -> Void) { diff --git a/Library/UIButton+HapticFeedback.swift b/Library/UIButton+HapticFeedback.swift deleted file mode 100644 index 74d24b3b8a..0000000000 --- a/Library/UIButton+HapticFeedback.swift +++ /dev/null @@ -1,15 +0,0 @@ -import UIKit - -extension UIButton { - public func generateSelectionFeedback() { - UIFeedbackGenerator.ksr_selection() - } - - public func generateSuccessFeedback() { - UIFeedbackGenerator.ksr_success() - } - - public func generateImpactFeedback(style: UIImpactFeedbackGenerator.FeedbackStyle) { - UIFeedbackGenerator.ksr_impact(style: style) - } -} diff --git a/Library/UIFeedbackGenerator+Extensions.swift b/Library/UIFeedbackGenerator+Extensions.swift deleted file mode 100644 index cd92bd8621..0000000000 --- a/Library/UIFeedbackGenerator+Extensions.swift +++ /dev/null @@ -1,15 +0,0 @@ -import UIKit - -extension UIFeedbackGenerator { - public static func ksr_success() { - UINotificationFeedbackGenerator().notificationOccurred(.success) - } - - public static func ksr_selection() { - UISelectionFeedbackGenerator().selectionChanged() - } - - public static func ksr_impact(style: UIImpactFeedbackGenerator.FeedbackStyle) { - UIImpactFeedbackGenerator(style: style).impactOccurred() - } -} diff --git a/Library/ViewModels/PledgeAmountCellViewModel.swift b/Library/ViewModels/PledgeAmountCellViewModel.swift index 2f9e7b9a86..07a981cc70 100644 --- a/Library/ViewModels/PledgeAmountCellViewModel.swift +++ b/Library/ViewModels/PledgeAmountCellViewModel.swift @@ -6,11 +6,17 @@ import ReactiveSwift public protocol PledgeAmountCellViewModelInputs { func configureWith(project: Project, reward: Reward) + func stepperValueChanged(_ value: Double) } public protocol PledgeAmountCellViewModelOutputs { var amount: Signal { get } var currency: Signal { get } + var generateSelectionFeedback: Signal { get } + var generateNotificationWarningFeedback: Signal { get } + var stepperInitialValue: Signal { get } + var stepperMaxValue: Signal { get } + var stepperMinValue: Signal { get } } public protocol PledgeAmountCellViewModelType { @@ -29,11 +35,42 @@ public final class PledgeAmountCellViewModel: PledgeAmountCellViewModelType, .skipNil() .map(second) - self.amount = reward - .map { String(format: "%.0f", $0.minimum) } + let initialValue = Signal.combineLatest(project, reward) + .map { _ in 15.0 } + + self.stepperInitialValue = initialValue + + self.amount = Signal.merge( + initialValue, + self.stepperValueProperty.signal + ) + .map { String(format: "%.0f", $0) } self.currency = project .map { currencySymbol(forCountry: $0.country).trimmed() } + + let minAndMax = Signal.combineLatest(project, reward) + .map { _ in (10.0, 20.0) } + + self.stepperMinValue = minAndMax.signal + .map(first) + + self.stepperMaxValue = minAndMax.signal + .map(second) + + let stepperValueChanged = Signal.combineLatest( + self.stepperMinValue.signal, + self.stepperMaxValue.signal, + self.stepperValueProperty.signal + ) + + self.generateSelectionFeedback = stepperValueChanged + .filter { min, max, value in min < value && value < max } + .ignoreValues() + + self.generateNotificationWarningFeedback = stepperValueChanged + .filter { min, max, value in value <= min || max <= value } + .ignoreValues() } private let projectAndRewardProperty = MutableProperty<(Project, Reward)?>(nil) @@ -41,8 +78,18 @@ public final class PledgeAmountCellViewModel: PledgeAmountCellViewModelType, self.projectAndRewardProperty.value = (project, reward) } + private let stepperValueProperty = MutableProperty(0) + public func stepperValueChanged(_ value: Double) { + self.stepperValueProperty.value = value + } + public let amount: Signal public let currency: Signal + public let generateSelectionFeedback: Signal + public let generateNotificationWarningFeedback: Signal + public let stepperInitialValue: Signal + public let stepperMaxValue: Signal + public let stepperMinValue: Signal public var inputs: PledgeAmountCellViewModelInputs { return self } public var outputs: PledgeAmountCellViewModelOutputs { return self } diff --git a/Library/ViewModels/PledgeAmountCellViewModelTests.swift b/Library/ViewModels/PledgeAmountCellViewModelTests.swift index 2fe3811afb..904f034695 100644 --- a/Library/ViewModels/PledgeAmountCellViewModelTests.swift +++ b/Library/ViewModels/PledgeAmountCellViewModelTests.swift @@ -9,29 +9,88 @@ internal final class PledgeAmountCellViewModelTests: TestCase { private let amount = TestObserver() private let currency = TestObserver() + private let generateSelectionFeedback = TestObserver() + private let generateNotificationWarningFeedback = TestObserver() + private let stepperInitialValue = TestObserver() + private let stepperMinValue = TestObserver() + private let stepperMaxValue = TestObserver() override func setUp() { super.setUp() self.vm.outputs.amount.observe(self.amount.observer) self.vm.outputs.currency.observe(self.currency.observer) + self.vm.outputs.generateSelectionFeedback.observe(self.generateSelectionFeedback.observer) + self.vm.outputs.generateNotificationWarningFeedback.observe( + self.generateNotificationWarningFeedback.observer + ) + self.vm.outputs.stepperInitialValue.observe(self.stepperInitialValue.observer) + self.vm.outputs.stepperMinValue.observe(self.stepperMinValue.observer) + self.vm.outputs.stepperMaxValue.observe(self.stepperMaxValue.observer) } func testAmountAndCurrency() { self.vm.inputs.configureWith(project: .template, reward: .template) - self.amount.assertValues(["10"]) + self.amount.assertValues(["15"]) self.currency.assertValues(["$"]) + } + + func testGenerateSelectionFeedback() { + self.vm.inputs.configureWith(project: .template, reward: .template) + + self.vm.inputs.stepperValueChanged(16) + self.generateSelectionFeedback.assertValueCount(1) + + self.vm.inputs.stepperValueChanged(20) + self.generateSelectionFeedback.assertValueCount(1) + + self.vm.inputs.stepperValueChanged(14) + self.generateSelectionFeedback.assertValueCount(2) + + self.vm.inputs.stepperValueChanged(10) + self.generateSelectionFeedback.assertValueCount(2) + + self.vm.inputs.stepperValueChanged(15) + self.generateSelectionFeedback.assertValueCount(3) + } + + func testGenerateNotificationWarningFeedback() { + self.vm.inputs.configureWith(project: .template, reward: .template) + + self.generateNotificationWarningFeedback.assertValueCount(0) + + self.vm.inputs.stepperValueChanged(16) + self.generateNotificationWarningFeedback.assertValueCount(0) - let project = Project.template - |> Project.lens.country .~ .jp + self.vm.inputs.stepperValueChanged(20) + self.generateNotificationWarningFeedback.assertValueCount(1) - let reward = Reward.template - |> Reward.lens.minimum .~ 200 + self.vm.inputs.stepperValueChanged(14) + self.generateNotificationWarningFeedback.assertValueCount(1) - self.vm.inputs.configureWith(project: project, reward: reward) + self.vm.inputs.stepperValueChanged(10) + self.generateNotificationWarningFeedback.assertValueCount(2) + + self.vm.inputs.stepperValueChanged(15) + self.generateNotificationWarningFeedback.assertValueCount(2) + } + + func testStepperInitialValue() { + self.vm.inputs.configureWith(project: .template, reward: .template) + + self.stepperInitialValue.assertValue(15) + } + + func testStepperMinValue() { + self.vm.inputs.configureWith(project: .template, reward: .template) + + self.stepperMinValue.assertValue(10) + } + + func testStepperMaxValue() { + self.vm.inputs.configureWith(project: .template, reward: .template) - self.amount.assertValues(["10", "200"]) - self.currency.assertValues(["$", "¥"]) + self.stepperMaxValue.assertValue(20) } } diff --git a/Library/ViewModels/WatchProjectViewModel.swift b/Library/ViewModels/WatchProjectViewModel.swift index 37054a9858..7257f9edaf 100644 --- a/Library/ViewModels/WatchProjectViewModel.swift +++ b/Library/ViewModels/WatchProjectViewModel.swift @@ -21,7 +21,7 @@ public protocol WatchProjectViewModelOutputs { var generateSelectionFeedback: Signal<(), Never> { get } /// Emits when haptic feedback should be generated - var generateSuccessFeedback: Signal<(), Never> { get } + var generateNotificationSuccessFeedback: Signal<(), Never> { get } /// Emits when the login tout should be shown to the user. var goToLoginTout: Signal<(), Never> { get } @@ -165,7 +165,7 @@ public final class WatchProjectViewModel: WatchProjectViewModelType, .skipRepeats() self.generateImpactFeedback = self.saveButtonTouchedProperty.signal - self.generateSuccessFeedback = saveButtonTapped.signal.filter(isFalse).ignoreValues() + self.generateNotificationSuccessFeedback = saveButtonTapped.signal.filter(isFalse).ignoreValues() self.generateSelectionFeedback = saveButtonTapped.signal.filter(isTrue).ignoreValues() self.saveButtonAccessibilityValue = self.saveButtonSelected @@ -219,7 +219,7 @@ public final class WatchProjectViewModel: WatchProjectViewModelType, } public let generateImpactFeedback: Signal<(), Never> - public let generateSuccessFeedback: Signal<(), Never> + public let generateNotificationSuccessFeedback: Signal<(), Never> public let generateSelectionFeedback: Signal<(), Never> public let goToLoginTout: Signal<(), Never> public let postNotificationWithProject: Signal diff --git a/Library/ViewModels/WatchProjectViewModelTests.swift b/Library/ViewModels/WatchProjectViewModelTests.swift index a9f4b5af76..6de38be861 100644 --- a/Library/ViewModels/WatchProjectViewModelTests.swift +++ b/Library/ViewModels/WatchProjectViewModelTests.swift @@ -9,7 +9,7 @@ internal final class WatchProjectViewModelTests: TestCase { internal let generateImpactFeedback = TestObserver<(), Never>() internal let generateSelectionFeedback = TestObserver<(), Never>() - internal let generateSuccessFeedback = TestObserver<(), Never>() + internal let generateNotificationSuccessFeedback = TestObserver<(), Never>() internal let goToLoginTout = TestObserver<(), Never>() internal let postNotificationWithProject = TestObserver() // fixme: test internal let saveButtonAccessibilityValue = TestObserver() @@ -22,7 +22,9 @@ internal final class WatchProjectViewModelTests: TestCase { self.vm.outputs.generateImpactFeedback.observe(self.generateImpactFeedback.observer) self.vm.outputs.generateSelectionFeedback.observe(self.generateSelectionFeedback.observer) - self.vm.outputs.generateSuccessFeedback.observe(self.generateSuccessFeedback.observer) + self.vm.outputs.generateNotificationSuccessFeedback.observe( + self.generateNotificationSuccessFeedback.observer + ) self.vm.outputs.goToLoginTout.observe(self.goToLoginTout.observer) self.vm.outputs.saveButtonAccessibilityValue.observe(self.saveButtonAccessibilityValue.observer) self.vm.outputs.saveButtonSelected.observe(self.saveButtonSelected.observer) @@ -58,11 +60,11 @@ internal final class WatchProjectViewModelTests: TestCase { self.vm.inputs.saveButtonTapped(selected: true) self.scheduler.advance() self.generateSelectionFeedback.assertValueCount(1) - self.generateSuccessFeedback.assertValueCount(0) + self.generateNotificationSuccessFeedback.assertValueCount(0) } } - func testGenerateSuccessFeedback() { + func testGenerateNotificationSuccessFeedback() { withEnvironment(currentUser: .template) { self.vm.inputs.configure(with: .template) self.vm.inputs.viewDidLoad() @@ -70,7 +72,7 @@ internal final class WatchProjectViewModelTests: TestCase { self.vm.inputs.saveButtonTapped(selected: false) self.scheduler.advance() self.generateSelectionFeedback.assertValueCount(0) - self.generateSuccessFeedback.assertValueCount(1) + self.generateNotificationSuccessFeedback.assertValueCount(1) } }