diff --git a/functional-reactive-intuition/Project/RFP.xcodeproj/project.pbxproj b/functional-reactive-intuition/Project/RFP.xcodeproj/project.pbxproj index 892fd22..fbe9795 100644 --- a/functional-reactive-intuition/Project/RFP.xcodeproj/project.pbxproj +++ b/functional-reactive-intuition/Project/RFP.xcodeproj/project.pbxproj @@ -16,6 +16,10 @@ 0C18B47D1C4454580081DFC5 /* ImperativeGestureReactorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C18B47C1C4454580081DFC5 /* ImperativeGestureReactorTests.swift */; }; 0C9FFB351C46625F00FD6C05 /* ReactiveViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C9FFB341C46625F00FD6C05 /* ReactiveViewController.swift */; }; 0CDDAB481C56C21C00D5DA3D /* ReactiveShortViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CDDAB471C56C21C00D5DA3D /* ReactiveShortViewController.swift */; }; + 1BF29EDA1C69E72D00912AD3 /* UIView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BF29ED91C69E72D00912AD3 /* UIView+Extensions.swift */; }; + 1F5B23AC1C8EBA7B00F28DE1 /* IntegratedReactiveGestureReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F5B23AB1C8EBA7B00F28DE1 /* IntegratedReactiveGestureReactor.swift */; }; + 1F5B23AE1C8EBCAD00F28DE1 /* IntegratedReactiveViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F5B23AD1C8EBCAD00F28DE1 /* IntegratedReactiveViewController.swift */; }; + 1F5B23B01C8EC62100F28DE1 /* IntegratedReactiveGestureReactorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F5B23AF1C8EC62100F28DE1 /* IntegratedReactiveGestureReactorTests.swift */; }; 1FB180CC1C6C72EF008BC2D1 /* UIGestureRecognizerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB180CB1C6C72EF008BC2D1 /* UIGestureRecognizerProtocol.swift */; }; 1FD5DC431C69D4B60050B3D9 /* ImperativeGestureReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD5DC421C69D4B60050B3D9 /* ImperativeGestureReactor.swift */; }; 1FD5DC451C69D60C0050B3D9 /* GestureReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD5DC441C69D60C0050B3D9 /* GestureReactor.swift */; }; @@ -24,7 +28,6 @@ 1FE3E4371C6CF6B800804CA2 /* ReactiveTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FE3E4361C6CF6B800804CA2 /* ReactiveTimer.swift */; }; 1FE3E4391C6DC6CC00804CA2 /* GestureReactorTestHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FE3E4381C6DC6CB00804CA2 /* GestureReactorTestHelper.swift */; }; 1FE3E43B1C6E40A200804CA2 /* ReactiveGestureReactorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FE3E43A1C6E40A200804CA2 /* ReactiveGestureReactorTests.swift */; }; - 1BF29EDA1C69E72D00912AD3 /* UIView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BF29ED91C69E72D00912AD3 /* UIView+Extensions.swift */; }; B48DEEE7DD9FCD9E8F77A13B /* Pods.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA3577BA56804CC07EC782AF /* Pods.framework */; }; /* End PBXBuildFile section */ @@ -53,6 +56,10 @@ 0C9FFB341C46625F00FD6C05 /* ReactiveViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactiveViewController.swift; sourceTree = ""; }; 0CDDAB471C56C21C00D5DA3D /* ReactiveShortViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactiveShortViewController.swift; sourceTree = ""; }; 1AFF0EC5CED5C7CDACEDF353 /* Pods.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Pods.debug.xcconfig; path = "Pods/Target Support Files/Pods/Pods.debug.xcconfig"; sourceTree = ""; }; + 1BF29ED91C69E72D00912AD3 /* UIView+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+Extensions.swift"; sourceTree = ""; }; + 1F5B23AB1C8EBA7B00F28DE1 /* IntegratedReactiveGestureReactor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IntegratedReactiveGestureReactor.swift; sourceTree = ""; }; + 1F5B23AD1C8EBCAD00F28DE1 /* IntegratedReactiveViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IntegratedReactiveViewController.swift; sourceTree = ""; }; + 1F5B23AF1C8EC62100F28DE1 /* IntegratedReactiveGestureReactorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IntegratedReactiveGestureReactorTests.swift; sourceTree = ""; }; 1FB180CB1C6C72EF008BC2D1 /* UIGestureRecognizerProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIGestureRecognizerProtocol.swift; sourceTree = ""; }; 1FD5DC421C69D4B60050B3D9 /* ImperativeGestureReactor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImperativeGestureReactor.swift; sourceTree = ""; }; 1FD5DC441C69D60C0050B3D9 /* GestureReactor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GestureReactor.swift; sourceTree = ""; }; @@ -61,7 +68,6 @@ 1FE3E4361C6CF6B800804CA2 /* ReactiveTimer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactiveTimer.swift; sourceTree = ""; }; 1FE3E4381C6DC6CB00804CA2 /* GestureReactorTestHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GestureReactorTestHelper.swift; sourceTree = ""; }; 1FE3E43A1C6E40A200804CA2 /* ReactiveGestureReactorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactiveGestureReactorTests.swift; sourceTree = ""; }; - 1BF29ED91C69E72D00912AD3 /* UIView+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+Extensions.swift"; sourceTree = ""; }; 350C6FD680F3FD2ADAEA261D /* Pods.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Pods.release.xcconfig; path = "Pods/Target Support Files/Pods/Pods.release.xcconfig"; sourceTree = ""; }; AA3577BA56804CC07EC782AF /* Pods.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -124,6 +130,8 @@ 1FB180CB1C6C72EF008BC2D1 /* UIGestureRecognizerProtocol.swift */, 1FE3E4341C6C782F00804CA2 /* TimerProtocol.swift */, 1FE3E4361C6CF6B800804CA2 /* ReactiveTimer.swift */, + 1F5B23AB1C8EBA7B00F28DE1 /* IntegratedReactiveGestureReactor.swift */, + 1F5B23AD1C8EBCAD00F28DE1 /* IntegratedReactiveViewController.swift */, ); path = RFP; sourceTree = ""; @@ -135,6 +143,7 @@ 0C18B47E1C4454580081DFC5 /* Info.plist */, 1FE3E4381C6DC6CB00804CA2 /* GestureReactorTestHelper.swift */, 1FE3E43A1C6E40A200804CA2 /* ReactiveGestureReactorTests.swift */, + 1F5B23AF1C8EC62100F28DE1 /* IntegratedReactiveGestureReactorTests.swift */, ); path = RFPTests; sourceTree = ""; @@ -315,6 +324,8 @@ 0C18B4681C4454580081DFC5 /* AppDelegate.swift in Sources */, 1FD5DC431C69D4B60050B3D9 /* ImperativeGestureReactor.swift in Sources */, 0C153D611C4F001300CBD947 /* Observable+Extension.swift in Sources */, + 1F5B23AC1C8EBA7B00F28DE1 /* IntegratedReactiveGestureReactor.swift in Sources */, + 1F5B23AE1C8EBCAD00F28DE1 /* IntegratedReactiveViewController.swift in Sources */, 0CDDAB481C56C21C00D5DA3D /* ReactiveShortViewController.swift in Sources */, 1FD5DC451C69D60C0050B3D9 /* GestureReactor.swift in Sources */, 1FE3E4351C6C782F00804CA2 /* TimerProtocol.swift in Sources */, @@ -329,6 +340,7 @@ files = ( 1FE3E4391C6DC6CC00804CA2 /* GestureReactorTestHelper.swift in Sources */, 1FE3E43B1C6E40A200804CA2 /* ReactiveGestureReactorTests.swift in Sources */, + 1F5B23B01C8EC62100F28DE1 /* IntegratedReactiveGestureReactorTests.swift in Sources */, 0C18B47D1C4454580081DFC5 /* ImperativeGestureReactorTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/functional-reactive-intuition/Project/RFP/IntegratedReactiveGestureReactor.swift b/functional-reactive-intuition/Project/RFP/IntegratedReactiveGestureReactor.swift new file mode 100644 index 0000000..ae81f2e --- /dev/null +++ b/functional-reactive-intuition/Project/RFP/IntegratedReactiveGestureReactor.swift @@ -0,0 +1,85 @@ +import Foundation +import UIKit +import RxSwift +import RxCocoa + + +// same implementation as ReactiveGestureReactor, but not using GestureReactor protocol, rather directly using Rx events +class IntegratedReactiveGestureReactor { + + var delegate: GestureReactorDelegate? + + private let timerCreator: ReactiveTimerCreator + private let disposeBag = DisposeBag() + + init(timerCreator: ReactiveTimerCreator, panGestureObservable: Observable, rotateGestureObservable: Observable) { + + self.timerCreator = timerCreator + + // FYI + // Passing on the UIGesture at this point is dodgy as it's a reference + // It's state will change and render our filter useless. + // We therefore keep just the state in our observable buffers [.Began,.Began,.Ended] + let rotateGesturesStartedEnded = rotateGestureObservable.filter { gesture in gesture.state == .Began || gesture.state == .Ended}.flatMap { (gesture) -> Observable in + return Observable.just(gesture.state) + } + + let panGesturesStartedEnded = panGestureObservable.filter { gesture in gesture.state == .Began || gesture.state == .Ended}.flatMap { (gesture) -> Observable in + return Observable.just(gesture.state) + } + + // Combine our latest .Began and .Ended from both Pan and Rotate. + // If they are the same then return the same state. If not then return a Failed. + let combineStartEndGestures = Observable.combineLatest(panGesturesStartedEnded, rotateGesturesStartedEnded) { (panState, rotateState) -> Observable in + + // If only one is .Ended, the result is .Ended too + var state = UIGestureRecognizerState.Ended + if panState == .Began && rotateState == .Began { + state = .Began + } + + return Observable.just(state) + }.switchLatest() + + // several .Began events in a row are to be treated the same as a single one, it has just meaning if a .Ended is in between + let distinctCombineStartEndGestures = combineStartEndGestures.distinctUntilChanged() + + + // condition: when both pan and rotate has begun + let bothGesturesStarted = distinctCombineStartEndGestures.filter { (state) -> Bool in + state == .Began + } + + // condition: when one of pan or rotate has Ended + let eitherGesturesEnded = distinctCombineStartEndGestures.filter { (state) -> Bool in + state == .Ended + } + + // when bothGesturesStarted, do this: + bothGesturesStarted.subscribeNext { [unowned self] _ in + + self.delegate?.didStart() + // create a timer that ticks every second + let timer = self.timerCreator(interval: 1) + // condition: but only three ticks + let timerThatTicksThree = timer.take(4) + // condition: and also, stop it immediately either pan or rotate ended + let timerThatTicksThreeAndStops = timerThatTicksThree.takeUntil(eitherGesturesEnded) + + timerThatTicksThreeAndStops.subscribe(onNext: { [unowned self] count in + // the imperative version waits for a second until didComplete is called, so we have to tick once more, but do not send the last tick to the delegate + guard count <= 2 else { + return + //do nothing + } + // when a tick happens, do this: + self.delegate?.didTick(2 - count) + }, onCompleted: { [unowned self] in + // when the timer completes, do this: + self.delegate?.didComplete() + }) + }.addDisposableTo(self.disposeBag) + + } + +} diff --git a/functional-reactive-intuition/Project/RFP/IntegratedReactiveViewController.swift b/functional-reactive-intuition/Project/RFP/IntegratedReactiveViewController.swift new file mode 100644 index 0000000..4a7be9a --- /dev/null +++ b/functional-reactive-intuition/Project/RFP/IntegratedReactiveViewController.swift @@ -0,0 +1,117 @@ +// +// ViewController.swift +// RFP +// +// Created by Mark Aron Szulyovszky on 11/01/2016. +// Copyright © 2016 Mark Aron Szulyovszky. All rights reserved. +// + +import UIKit +import RxSwift +import RxCocoa + +class IntegratedReactiveViewController: UIViewController, SetStatus, GestureReactorDelegate { + + @IBOutlet weak var draggableView: UIView! + @IBOutlet weak var statusLabel: UILabel! + @IBOutlet weak var centerXConstraint: NSLayoutConstraint! //For updating the position of the box when dragging + @IBOutlet weak var centerYConstraint: NSLayoutConstraint! + + private let pan: UIPanGestureRecognizer + private let rotate: UIRotationGestureRecognizer + private var gestureReactor: IntegratedReactiveGestureReactor + + private let disposeBag = DisposeBag() + + required init?(coder aDecoder: NSCoder) { + pan = UIPanGestureRecognizer() + rotate = UIRotationGestureRecognizer() + + // workaround to convert ControlEvent to Observable + let panObservable: Observable = pan.rx_event.asObservable().flatMap { gesture -> Observable in + return Observable.just(gesture as UIGestureRecognizerType) + } + let rotateObservable: Observable = rotate.rx_event.asObservable().flatMap { gesture -> Observable in + return Observable.just(gesture as UIGestureRecognizerType) + } + + gestureReactor = IntegratedReactiveGestureReactor(timerCreator: { interval in ReactiveTimerFactory.reactiveTimer(interval: interval) }, panGestureObservable: panObservable, rotateGestureObservable: rotateObservable) + + super.init(coder: aDecoder) + } + + // TODO as we like to have non-optional and non-implicitly-unwrapped properties, we need to execute the setup code in both initializers - unfortunately we can not call instance helper functions here with the current version of swift + override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?) { + pan = UIPanGestureRecognizer() + rotate = UIRotationGestureRecognizer() + + // workaround to convert ControlEvent to Observable + let panObservable: Observable = pan.rx_event.asObservable().flatMap { gesture -> Observable in + return Observable.just(gesture as UIGestureRecognizerType) + } + let rotateObservable: Observable = rotate.rx_event.asObservable().flatMap { gesture -> Observable in + return Observable.just(gesture as UIGestureRecognizerType) + } + + gestureReactor = IntegratedReactiveGestureReactor(timerCreator: { interval in ReactiveTimerFactory.reactiveTimer(interval: interval) }, panGestureObservable: panObservable, rotateGestureObservable: rotateObservable) + + super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) + } + + override func viewDidLoad() { + super.viewDidLoad() + + gestureReactor.delegate = self + + self.draggableView.gestureRecognizers = [pan, rotate] + + + /// + /// + /// Extra Code to manipulate move and rotate the subview. + /// + /// Uses custom infix on CGPoint to '-' or '+' two together. + + let panLocation = pan.rx_event.map { [unowned self] in + $0.locationInView(self.view) - self.view.center + } + panLocation.map { $0.x } + .bindTo(self.centerXConstraint.rx_constant) + .addDisposableTo(self.disposeBag) + + panLocation.map { $0.y } + .bindTo(self.centerYConstraint.rx_constant) + .addDisposableTo(self.disposeBag) + + rotate.rx_event + .map { ($0 as! UIRotationGestureRecognizer).rotation } + .bindTo(self.draggableView.rx_rotate) + .addDisposableTo(self.disposeBag) + } + + override func viewWillAppear(animated: Bool) { + super.viewWillAppear(animated) + self.setStatus("Status: Waiting for Rotate & Pan") + } + + func didStart() { + self.setStatus("Started") + } + + func didTick(secondsLeft: Int) { + self.setStatus("Tick: \(secondsLeft)") + } + + func didComplete() { + self.setStatus("Completed") + } + +} + +extension IntegratedReactiveViewController: UIGestureRecognizerDelegate { + + func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWithGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } + +} diff --git a/functional-reactive-intuition/Project/RFPTests/IntegratedReactiveGestureReactorTests.swift b/functional-reactive-intuition/Project/RFPTests/IntegratedReactiveGestureReactorTests.swift new file mode 100644 index 0000000..94d61b8 --- /dev/null +++ b/functional-reactive-intuition/Project/RFPTests/IntegratedReactiveGestureReactorTests.swift @@ -0,0 +1,404 @@ +import XCTest +import RxSwift +import RxCocoa +@testable import RFP + + +class IntegratedReactiveGestureReactorTests: XCTestCase { + + var sut: IntegratedReactiveGestureReactor! + var mockDelegate: MockGestureReactorDelegate! + var mockPanGestureObservable: Observable! + var mockRotateGestureObservable: Observable! + var mockPanVariable: Variable! + var mockRotateVariable: Variable! + var mockTimerCreatorCalled = 0 + // FIXME cannot be weak, so we cannot test the same way as in ImperativeGestureReactorTests + var mockTimer: MockReactiveTimer? + + override func setUp() { + super.setUp() + mockPanVariable = Variable(MockPanGestureRecognizer(state: .Possible)) + mockRotateVariable = Variable(MockRotateGestureRecognizer(state: .Possible)) + mockPanGestureObservable = mockPanVariable.asObservable().skip(1) + mockRotateGestureObservable = mockRotateVariable.asObservable().skip(1) + let timerCreator: ReactiveTimerCreator = { [unowned self] interval in + self.mockTimerCreatorCalled += 1 + let mockTimer = MockReactiveTimer(interval: interval) + self.mockTimer = mockTimer + return mockTimer.asObservable().skip(1) + } + sut = IntegratedReactiveGestureReactor(timerCreator: timerCreator, panGestureObservable: mockPanGestureObservable, rotateGestureObservable: mockRotateGestureObservable) + mockDelegate = MockGestureReactorDelegate() + sut.delegate = mockDelegate + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + super.tearDown() + } + + func testDoNothing() { + XCTAssertNil(mockTimer) + XCTAssertEqual(mockDelegate.didStartCalled, 0) + XCTAssertEqual(mockDelegate.didTickCalled, 0) + XCTAssertEqual(mockDelegate.didCompleteCalled, 0) + XCTAssertEqual(mockDelegate.tickSecondsLefts, []) + XCTAssertEqual(mockTimerCreatorCalled, 0) + XCTAssertNil(mockTimer) + } + + func testBeganPanGesture() { + mockPanVariable.value = MockPanGestureRecognizer(state: .Began) + + XCTAssertEqual(mockDelegate.didStartCalled, 0) + XCTAssertEqual(mockDelegate.didTickCalled, 0) + XCTAssertEqual(mockDelegate.didCompleteCalled, 0) + XCTAssertEqual(mockDelegate.tickSecondsLefts, []) + XCTAssertEqual(mockTimerCreatorCalled, 0) + XCTAssertNil(mockTimer) + } + + func testBeganRotateGesture() { + mockRotateVariable.value = MockRotateGestureRecognizer(state: .Began) + + XCTAssertEqual(mockDelegate.didStartCalled, 0) + XCTAssertEqual(mockDelegate.didTickCalled, 0) + XCTAssertEqual(mockDelegate.didCompleteCalled, 0) + XCTAssertEqual(mockDelegate.tickSecondsLefts, []) + XCTAssertEqual(mockTimerCreatorCalled, 0) + XCTAssertNil(mockTimer) + } + + func testBeganPanEndedPanBeganRotateGesture() { + mockPanVariable.value = MockPanGestureRecognizer(state: .Began) + mockPanVariable.value = MockPanGestureRecognizer(state: .Ended) + mockRotateVariable.value = MockRotateGestureRecognizer(state: .Began) + + XCTAssertEqual(mockDelegate.didStartCalled, 0) + XCTAssertEqual(mockDelegate.didTickCalled, 0) + XCTAssertEqual(mockDelegate.didCompleteCalled, 0) + XCTAssertEqual(mockDelegate.tickSecondsLefts, []) + XCTAssertEqual(mockTimerCreatorCalled, 0) + XCTAssertNil(mockTimer) + } + + func testBeganBothGestures() { + mockPanVariable.value = MockPanGestureRecognizer(state: .Began) + mockRotateVariable.value = MockRotateGestureRecognizer(state: .Began) + + XCTAssertEqual(mockDelegate.didStartCalled, 1) + XCTAssertEqual(mockDelegate.didTickCalled, 0) + XCTAssertEqual(mockDelegate.didCompleteCalled, 0) + XCTAssertEqual(mockDelegate.tickSecondsLefts, []) + XCTAssertEqual(mockTimerCreatorCalled, 1) + XCTAssertNotNil(mockTimer) + } + + func testBeganBothGesturesAndEndedRotate() { + mockPanVariable.value = MockPanGestureRecognizer(state: .Began) + mockRotateVariable.value = MockRotateGestureRecognizer(state: .Began) + mockRotateVariable.value = MockRotateGestureRecognizer(state: .Ended) + + XCTAssertEqual(mockDelegate.didStartCalled, 1) + XCTAssertEqual(mockDelegate.didTickCalled, 0) + XCTAssertEqual(mockDelegate.didCompleteCalled, 1) + XCTAssertEqual(mockDelegate.tickSecondsLefts, []) + XCTAssertEqual(mockTimerCreatorCalled, 1) + // XCTAssertNil(mockTimer) + } + + func testBeganBothGesturesAndTickedOnce() { + mockPanVariable.value = MockPanGestureRecognizer(state: .Began) + mockRotateVariable.value = MockRotateGestureRecognizer(state: .Began) + mockTimer!.mockExecuteOnTick() + + XCTAssertEqual(mockDelegate.didStartCalled, 1) + XCTAssertEqual(mockDelegate.didTickCalled, 1) + XCTAssertEqual(mockDelegate.didCompleteCalled, 0) + XCTAssertEqual(mockDelegate.tickSecondsLefts, [2]) + XCTAssertEqual(mockTimerCreatorCalled, 1) + // XCTAssertNotNil(mockTimer) + } + + func testBeganBothGesturesAndTickedOnceAndEndedRotate() { + mockPanVariable.value = MockPanGestureRecognizer(state: .Began) + mockRotateVariable.value = MockRotateGestureRecognizer(state: .Began) + mockTimer!.mockExecuteOnTick() + mockRotateVariable.value = MockRotateGestureRecognizer(state: .Ended) + + XCTAssertEqual(mockDelegate.didStartCalled, 1) + XCTAssertEqual(mockDelegate.didTickCalled, 1) + XCTAssertEqual(mockDelegate.didCompleteCalled, 1) + XCTAssertEqual(mockDelegate.tickSecondsLefts, [2]) + XCTAssertEqual(mockTimerCreatorCalled, 1) + // XCTAssertNil(mockTimer) + } + + func testBeganBothGesturesAndTickedTwice() { + mockPanVariable.value = MockPanGestureRecognizer(state: .Began) + mockRotateVariable.value = MockRotateGestureRecognizer(state: .Began) + mockTimer!.mockExecuteOnTick() + mockTimer!.mockExecuteOnTick() + + XCTAssertEqual(mockDelegate.didStartCalled, 1) + XCTAssertEqual(mockDelegate.didTickCalled, 2) + XCTAssertEqual(mockDelegate.didCompleteCalled, 0) + XCTAssertEqual(mockDelegate.tickSecondsLefts, [2, 1]) + XCTAssertEqual(mockTimerCreatorCalled, 1) + // XCTAssertNotNil(mockTimer) + } + + func testBeganBothGesturesAndTickedThrice() { + mockPanVariable.value = MockPanGestureRecognizer(state: .Began) + mockRotateVariable.value = MockRotateGestureRecognizer(state: .Began) + mockTimer!.mockExecuteOnTick() + mockTimer!.mockExecuteOnTick() + mockTimer!.mockExecuteOnTick() + + XCTAssertEqual(mockDelegate.didStartCalled, 1) + XCTAssertEqual(mockDelegate.didTickCalled, 3) + XCTAssertEqual(mockDelegate.didCompleteCalled, 0) + XCTAssertEqual(mockDelegate.tickSecondsLefts, [2, 1, 0]) + XCTAssertEqual(mockTimerCreatorCalled, 1) + // XCTAssertNotNil(mockTimer) + } + + func testBeganBothGesturesAndTickedFrice() { + mockPanVariable.value = MockPanGestureRecognizer(state: .Began) + mockRotateVariable.value = MockRotateGestureRecognizer(state: .Began) + mockTimer!.mockExecuteOnTick() + mockTimer!.mockExecuteOnTick() + mockTimer!.mockExecuteOnTick() + mockTimer!.mockExecuteOnTick() + + XCTAssertEqual(mockDelegate.didStartCalled, 1) + XCTAssertEqual(mockDelegate.didTickCalled, 3) + XCTAssertEqual(mockDelegate.didCompleteCalled, 1) + XCTAssertEqual(mockDelegate.tickSecondsLefts, [2, 1, 0]) + XCTAssertEqual(mockTimerCreatorCalled, 1) + // XCTAssertNil(mockTimer) + } + + func testBeganBothGesturesAndTickedFriceAndPanEnded() { + mockPanVariable.value = MockPanGestureRecognizer(state: .Began) + mockRotateVariable.value = MockRotateGestureRecognizer(state: .Began) + mockTimer!.mockExecuteOnTick() + mockTimer!.mockExecuteOnTick() + mockTimer!.mockExecuteOnTick() + mockTimer!.mockExecuteOnTick() + mockPanVariable.value = MockPanGestureRecognizer(state: .Ended) + + XCTAssertEqual(mockDelegate.didStartCalled, 1) + XCTAssertEqual(mockDelegate.didTickCalled, 3) + XCTAssertEqual(mockDelegate.didCompleteCalled, 1) + XCTAssertEqual(mockDelegate.tickSecondsLefts, [2, 1, 0]) + XCTAssertEqual(mockTimerCreatorCalled, 1) + // XCTAssertNil(mockTimer) + } + + func testBeganBothGesturesAndTickedTwiceAndPanEndedAndPanBeganAgain() { + mockPanVariable.value = MockPanGestureRecognizer(state: .Began) + mockRotateVariable.value = MockRotateGestureRecognizer(state: .Began) + mockTimer!.mockExecuteOnTick() + mockTimer!.mockExecuteOnTick() + mockPanVariable.value = MockPanGestureRecognizer(state: .Ended) + mockPanVariable.value = MockPanGestureRecognizer(state: .Began) + + XCTAssertEqual(mockDelegate.didStartCalled, 2) + XCTAssertEqual(mockDelegate.didTickCalled, 2) + XCTAssertEqual(mockDelegate.didCompleteCalled, 1) + XCTAssertEqual(mockDelegate.tickSecondsLefts, [2, 1]) + XCTAssertEqual(mockTimerCreatorCalled, 2) + // XCTAssertNotNil(mockTimer) + } + + func testBeganBothGesturesAndPanBeganAgain_ignoreAdditionalBegans() { + mockPanVariable.value = MockPanGestureRecognizer(state: .Began) + mockRotateVariable.value = MockRotateGestureRecognizer(state: .Began) + mockPanVariable.value = MockPanGestureRecognizer(state: .Began) + mockPanVariable.value = MockPanGestureRecognizer(state: .Began) + mockPanVariable.value = MockPanGestureRecognizer(state: .Began) + mockPanVariable.value = MockPanGestureRecognizer(state: .Began) + + XCTAssertEqual(mockDelegate.didStartCalled, 1) + XCTAssertEqual(mockDelegate.didTickCalled, 0) + XCTAssertEqual(mockDelegate.didCompleteCalled, 0) + XCTAssertEqual(mockDelegate.tickSecondsLefts, []) + XCTAssertEqual(mockTimerCreatorCalled, 1) + // XCTAssertNotNil(mockTimer) + } + + func testBeganBothGesturesAndTickedFriceAndPanBeganAgain() { + mockPanVariable.value = MockPanGestureRecognizer(state: .Began) + mockRotateVariable.value = MockRotateGestureRecognizer(state: .Began) + mockTimer!.mockExecuteOnTick() + mockTimer!.mockExecuteOnTick() + mockTimer!.mockExecuteOnTick() + mockTimer!.mockExecuteOnTick() + mockPanVariable.value = MockPanGestureRecognizer(state: .Began) + + XCTAssertEqual(mockDelegate.didStartCalled, 1) + XCTAssertEqual(mockDelegate.didTickCalled, 3) + XCTAssertEqual(mockDelegate.didCompleteCalled, 1) + XCTAssertEqual(mockDelegate.tickSecondsLefts, [2, 1, 0]) + XCTAssertEqual(mockTimerCreatorCalled, 1) + // XCTAssertNil(mockTimer) + } + + func testBeganBothGesturesAndTickedFriceAndEndedPanGestureAndBeganPanAgain() { + mockPanVariable.value = MockPanGestureRecognizer(state: .Began) + mockRotateVariable.value = MockRotateGestureRecognizer(state: .Began) + mockTimer!.mockExecuteOnTick() + mockTimer!.mockExecuteOnTick() + mockTimer!.mockExecuteOnTick() + mockTimer!.mockExecuteOnTick() + mockPanVariable.value = MockPanGestureRecognizer(state: .Ended) + mockPanVariable.value = MockPanGestureRecognizer(state: .Began) + + XCTAssertEqual(mockDelegate.didStartCalled, 2) + XCTAssertEqual(mockDelegate.didTickCalled, 3) + XCTAssertEqual(mockDelegate.didCompleteCalled, 1) + XCTAssertEqual(mockDelegate.tickSecondsLefts, [2, 1, 0]) + XCTAssertEqual(mockTimerCreatorCalled, 2) + // XCTAssertNotNil(mockTimer) + } + + func testBeganBothGesturesAndTickedFriceAndEndedRotateGestureAndBeganRotateAgain() { + mockPanVariable.value = MockPanGestureRecognizer(state: .Began) + mockRotateVariable.value = MockRotateGestureRecognizer(state: .Began) + mockTimer!.mockExecuteOnTick() + mockTimer!.mockExecuteOnTick() + mockTimer!.mockExecuteOnTick() + mockTimer!.mockExecuteOnTick() + mockRotateVariable.value = MockRotateGestureRecognizer(state: .Ended) + mockRotateVariable.value = MockRotateGestureRecognizer(state: .Began) + + XCTAssertEqual(mockDelegate.didStartCalled, 2) + XCTAssertEqual(mockDelegate.didTickCalled, 3) + XCTAssertEqual(mockDelegate.didCompleteCalled, 1) + XCTAssertEqual(mockDelegate.tickSecondsLefts, [2, 1, 0]) + XCTAssertEqual(mockTimerCreatorCalled, 2) + // XCTAssertNotNil(mockTimer) + } + + func testBeganBothGesturesAndTickedFriceAndEndedBothGesturesAndBeganBothAgain() { + mockPanVariable.value = MockPanGestureRecognizer(state: .Began) + mockRotateVariable.value = MockRotateGestureRecognizer(state: .Began) + mockTimer!.mockExecuteOnTick() + mockTimer!.mockExecuteOnTick() + mockTimer!.mockExecuteOnTick() + mockTimer!.mockExecuteOnTick() + mockRotateVariable.value = MockRotateGestureRecognizer(state: .Ended) + mockPanVariable.value = MockPanGestureRecognizer(state: .Ended) + mockRotateVariable.value = MockRotateGestureRecognizer(state: .Began) + mockPanVariable.value = MockPanGestureRecognizer(state: .Began) + + XCTAssertEqual(mockDelegate.didStartCalled, 2) + XCTAssertEqual(mockDelegate.didTickCalled, 3) + XCTAssertEqual(mockDelegate.didCompleteCalled, 1) + XCTAssertEqual(mockDelegate.tickSecondsLefts, [2, 1, 0]) + XCTAssertEqual(mockTimerCreatorCalled, 2) + // XCTAssertNotNil(mockTimer) + } + + func testBeganBothGesturesAndTickedFriceAndEndedBothGesturesAndBeganBothAgainAndTickedOnce() { + mockPanVariable.value = MockPanGestureRecognizer(state: .Began) + mockRotateVariable.value = MockRotateGestureRecognizer(state: .Began) + mockTimer!.mockExecuteOnTick() + mockTimer!.mockExecuteOnTick() + mockTimer!.mockExecuteOnTick() + mockTimer!.mockExecuteOnTick() + mockRotateVariable.value = MockRotateGestureRecognizer(state: .Ended) + mockPanVariable.value = MockPanGestureRecognizer(state: .Ended) + mockRotateVariable.value = MockRotateGestureRecognizer(state: .Began) + mockPanVariable.value = MockPanGestureRecognizer(state: .Began) + mockTimer!.mockExecuteOnTick() + + XCTAssertEqual(mockDelegate.didStartCalled, 2) + XCTAssertEqual(mockDelegate.didTickCalled, 4) + XCTAssertEqual(mockDelegate.didCompleteCalled, 1) + XCTAssertEqual(mockDelegate.tickSecondsLefts, [2, 1, 0, 2]) + XCTAssertEqual(mockTimerCreatorCalled, 2) + // XCTAssertNotNil(mockTimer) + } + + func testBeganBothGesturesAndTickedFriceAndEndedBothGesturesAndBeganBothAgainAndTickedFrice() { + mockPanVariable.value = MockPanGestureRecognizer(state: .Began) + mockRotateVariable.value = MockRotateGestureRecognizer(state: .Began) + mockTimer!.mockExecuteOnTick() + mockTimer!.mockExecuteOnTick() + mockTimer!.mockExecuteOnTick() + mockTimer!.mockExecuteOnTick() + mockRotateVariable.value = MockRotateGestureRecognizer(state: .Ended) + mockPanVariable.value = MockPanGestureRecognizer(state: .Ended) + mockRotateVariable.value = MockRotateGestureRecognizer(state: .Began) + mockPanVariable.value = MockPanGestureRecognizer(state: .Began) + mockTimer!.mockExecuteOnTick() + mockTimer!.mockExecuteOnTick() + mockTimer!.mockExecuteOnTick() + mockTimer!.mockExecuteOnTick() + + XCTAssertEqual(mockDelegate.didStartCalled, 2) + XCTAssertEqual(mockDelegate.didTickCalled, 6) + XCTAssertEqual(mockDelegate.didCompleteCalled, 2) + XCTAssertEqual(mockDelegate.tickSecondsLefts, [2, 1, 0, 2, 1, 0]) + XCTAssertEqual(mockTimerCreatorCalled, 2) + // XCTAssertNil(mockTimer) + } + + func testBeganBothGesturesAndTickedFriceAndEndedBothGesturesAndBeganBothAgainAndTickedFriceAndEndedBothGestures() { + mockPanVariable.value = MockPanGestureRecognizer(state: .Began) + mockRotateVariable.value = MockRotateGestureRecognizer(state: .Began) + mockTimer!.mockExecuteOnTick() + mockTimer!.mockExecuteOnTick() + mockTimer!.mockExecuteOnTick() + mockTimer!.mockExecuteOnTick() + mockRotateVariable.value = MockRotateGestureRecognizer(state: .Ended) + mockPanVariable.value = MockPanGestureRecognizer(state: .Ended) + mockRotateVariable.value = MockRotateGestureRecognizer(state: .Began) + mockPanVariable.value = MockPanGestureRecognizer(state: .Began) + mockTimer!.mockExecuteOnTick() + mockTimer!.mockExecuteOnTick() + mockTimer!.mockExecuteOnTick() + mockTimer!.mockExecuteOnTick() + mockRotateVariable.value = MockRotateGestureRecognizer(state: .Ended) + mockPanVariable.value = MockPanGestureRecognizer(state: .Ended) + + XCTAssertEqual(mockDelegate.didStartCalled, 2) + XCTAssertEqual(mockDelegate.didTickCalled, 6) + XCTAssertEqual(mockDelegate.didCompleteCalled, 2) + XCTAssertEqual(mockDelegate.tickSecondsLefts, [2, 1, 0, 2, 1, 0]) + XCTAssertEqual(mockTimerCreatorCalled, 2) + // XCTAssertNil(mockTimer) + } + + func testBeganBothGesturesAndTickedFriceAndEndedBothGesturesAndBeganBothAgainAndTickedFriceAndEndedBothGesturesAndStartedBothAgain() { + mockPanVariable.value = MockPanGestureRecognizer(state: .Began) + mockRotateVariable.value = MockRotateGestureRecognizer(state: .Began) + mockTimer!.mockExecuteOnTick() + mockTimer!.mockExecuteOnTick() + mockTimer!.mockExecuteOnTick() + mockTimer!.mockExecuteOnTick() + mockRotateVariable.value = MockRotateGestureRecognizer(state: .Ended) + mockPanVariable.value = MockPanGestureRecognizer(state: .Ended) + mockRotateVariable.value = MockRotateGestureRecognizer(state: .Began) + mockPanVariable.value = MockPanGestureRecognizer(state: .Began) + mockTimer!.mockExecuteOnTick() + mockTimer!.mockExecuteOnTick() + mockTimer!.mockExecuteOnTick() + mockTimer!.mockExecuteOnTick() + mockRotateVariable.value = MockRotateGestureRecognizer(state: .Ended) + mockPanVariable.value = MockPanGestureRecognizer(state: .Ended) + mockRotateVariable.value = MockRotateGestureRecognizer(state: .Began) + mockPanVariable.value = MockPanGestureRecognizer(state: .Began) + + XCTAssertEqual(mockDelegate.didStartCalled, 3) + XCTAssertEqual(mockDelegate.didTickCalled, 6) + XCTAssertEqual(mockDelegate.didCompleteCalled, 2) + XCTAssertEqual(mockDelegate.tickSecondsLefts, [2, 1, 0, 2, 1, 0]) + XCTAssertEqual(mockTimerCreatorCalled, 3) + // XCTAssertNotNil(mockTimer) + } + +}