From d31e82f2a4e66b172ead34c6be945fdf5d01b37e Mon Sep 17 00:00:00 2001 From: Mark Aron Szulyovszky Date: Sun, 6 Mar 2016 11:47:18 +0100 Subject: [PATCH 1/2] refactor: using map instread of flatMap where appropriate, initializing ReactiveGestureReactor with UIGestureRecognizers --- .../Project/RFP/ReactiveGestureReactor.swift | 52 +++++++------------ .../Project/RFP/ReactiveViewController.swift | 11 ++-- 2 files changed, 24 insertions(+), 39 deletions(-) diff --git a/functional-reactive-intuition/Project/RFP/ReactiveGestureReactor.swift b/functional-reactive-intuition/Project/RFP/ReactiveGestureReactor.swift index fe7d30c..e4f5197 100644 --- a/functional-reactive-intuition/Project/RFP/ReactiveGestureReactor.swift +++ b/functional-reactive-intuition/Project/RFP/ReactiveGestureReactor.swift @@ -8,56 +8,40 @@ class ReactiveGestureReactor: GestureReactor { var delegate: GestureReactorDelegate? - private var timerCreator: ReactiveTimerCreator + private let timerCreator: ReactiveTimerCreator private let disposeBag = DisposeBag() - private var panVariable: Variable - private var rotateVariable: Variable + private let panVariable: Variable + private let rotateVariable: Variable - init(timerCreator: ReactiveTimerCreator) { + init(timerCreator: ReactiveTimerCreator, gestureRecognizers: (UIGestureRecognizerType, UIGestureRecognizerType)) { self.timerCreator = timerCreator - panVariable = Variable(nil) - rotateVariable = Variable(nil) - + self.panVariable = Variable(gestureRecognizers.0) + self.rotateVariable = Variable(gestureRecognizers.1) // 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 = rotateVariable.asObservable().filter { gesture in gesture?.state == .Began || gesture?.state == .Ended}.flatMap { (gesture) -> Observable in - return Observable.just(gesture!.state) - } - let panGesturesStartedEnded = panVariable.asObservable().filter { gesture in gesture?.state == .Began || gesture?.state == .Ended}.flatMap { (gesture) -> Observable in - return Observable.just(gesture!.state) - } + let rotateGesturesStartedEnded = rotateVariable.asObservable().map { $0.state }.filter { $0 != .Began || $0 != .Ended } + let panGesturesStartedEnded = panVariable.asObservable().map { $0.state }.filter { $0 != .Began || $0 != .Ended } // 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 distinceCombineStartEndGestures = combineStartEndGestures.distinctUntilChanged() - + // If they are the same then return the same state. If not then return .Ended. + let combinedGesture = Observable + .combineLatest(rotateGesturesStartedEnded, panGesturesStartedEnded) { ($0, $1) } + .map { ($0.0 == .Began && $0.1 == .Began) + ? UIGestureRecognizerState.Began + : UIGestureRecognizerState.Ended + }.distinctUntilChanged() + // 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 // condition: when both pan and rotate has begun - let bothGesturesStarted = distinceCombineStartEndGestures.filter { (state) -> Bool in - state == .Began - } + let bothGesturesStarted = combinedGesture.filter { $0 == .Began } // condition: when both pan and rotate has Ended - let bothGesturesEnded = distinceCombineStartEndGestures.filter { (state) -> Bool in - state == .Ended - } + let bothGesturesEnded = combinedGesture.filter { $0 == .Ended } // when bothGesturesStarted, do this: bothGesturesStarted.subscribeNext { [unowned self] _ in diff --git a/functional-reactive-intuition/Project/RFP/ReactiveViewController.swift b/functional-reactive-intuition/Project/RFP/ReactiveViewController.swift index 1756366..b7e423a 100644 --- a/functional-reactive-intuition/Project/RFP/ReactiveViewController.swift +++ b/functional-reactive-intuition/Project/RFP/ReactiveViewController.swift @@ -17,21 +17,22 @@ class ReactiveViewController: UIViewController, SetStatus, GestureReactorDelegat @IBOutlet weak var centerXConstraint: NSLayoutConstraint! //For updating the position of the box when dragging @IBOutlet weak var centerYConstraint: NSLayoutConstraint! - private var gestureReactor: GestureReactor = ReactiveGestureReactor(timerCreator: { interval in ReactiveTimerFactory.reactiveTimer(interval: interval) }) + private var gestureReactor: GestureReactor! private let disposeBag = DisposeBag() override func viewDidLoad() { super.viewDidLoad() - gestureReactor.delegate = self - let pan = UIPanGestureRecognizer(target: self, action: "handlePan:") pan.delegate = self let rotate = UIRotationGestureRecognizer(target: self, action: "handleRotate:") rotate.delegate = self - self.draggableView.gestureRecognizers = [pan, rotate] + draggableView.gestureRecognizers = [pan, rotate] + gestureReactor = ReactiveGestureReactor(timerCreator: { interval in ReactiveTimerFactory.reactiveTimer(interval: interval) }, gestureRecognizers: (pan, rotate)) + gestureReactor.delegate = self + /// /// @@ -39,7 +40,7 @@ class ReactiveViewController: UIViewController, SetStatus, GestureReactorDelegat /// /// Uses custom infix on CGPoint to '-' or '+' two together. - let panLocation = pan.rx_event.map { $0.locationInView(self.view) - self.view.center } + 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) From 78f39dcf3802e8ddf5402d72b672bbb8a6658946 Mon Sep 17 00:00:00 2001 From: Mark Aron Szulyovszky Date: Sun, 6 Mar 2016 12:14:51 +0100 Subject: [PATCH 2/2] fix: correct ticking behaviour for ReactiveGestureReactor --- .../Project/RFP/ReactiveGestureReactor.swift | 31 ++++++++++++------- .../ReactiveGestureReactorTests.swift | 2 +- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/functional-reactive-intuition/Project/RFP/ReactiveGestureReactor.swift b/functional-reactive-intuition/Project/RFP/ReactiveGestureReactor.swift index e4f5197..dd9d62d 100644 --- a/functional-reactive-intuition/Project/RFP/ReactiveGestureReactor.swift +++ b/functional-reactive-intuition/Project/RFP/ReactiveGestureReactor.swift @@ -24,8 +24,13 @@ class ReactiveGestureReactor: GestureReactor { // 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 = rotateVariable.asObservable().map { $0.state }.filter { $0 != .Began || $0 != .Ended } - let panGesturesStartedEnded = panVariable.asObservable().map { $0.state }.filter { $0 != .Began || $0 != .Ended } + let rotateGesturesStartedEnded = rotateVariable.asObservable() + .map { $0.state } + .filter { $0 == .Began || $0 == .Ended } + + let panGesturesStartedEnded = panVariable.asObservable() + .map { $0.state } + .filter { $0 == .Began || $0 == .Ended } // 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 .Ended. @@ -34,7 +39,11 @@ class ReactiveGestureReactor: GestureReactor { .map { ($0.0 == .Began && $0.1 == .Began) ? UIGestureRecognizerState.Began : UIGestureRecognizerState.Ended - }.distinctUntilChanged() + } + .map { state -> UIGestureRecognizerState in + print(state) + return state } + .distinctUntilChanged() // 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 // condition: when both pan and rotate has begun @@ -54,18 +63,16 @@ class ReactiveGestureReactor: GestureReactor { // condition: and also, stop it immediately when both pan and rotate ended let timerThatTicksThreeAndStops = timerThatTicksThree.takeUntil(bothGesturesEnded) - 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 < 4 else { - return - //do nothing - } - // when a tick happens, do this: - self.delegate?.didTick(3 - count) + timerThatTicksThreeAndStops + // 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 + .filter { $0 < 3 } + .subscribe(onNext: { [unowned self] count in + // 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/RFPTests/ReactiveGestureReactorTests.swift b/functional-reactive-intuition/Project/RFPTests/ReactiveGestureReactorTests.swift index 9cd736f..dc4332c 100644 --- a/functional-reactive-intuition/Project/RFPTests/ReactiveGestureReactorTests.swift +++ b/functional-reactive-intuition/Project/RFPTests/ReactiveGestureReactorTests.swift @@ -19,7 +19,7 @@ class ReactiveGestureReactorTests: XCTestCase { self.mockTimer = mockTimer return mockTimer.asObservable().skip(1) } - sut = ReactiveGestureReactor(timerCreator: timerCreator) + sut = ReactiveGestureReactor(timerCreator: timerCreator, gestureRecognizers: (MockPanGestureRecognizer(state: .Cancelled), MockRotateGestureRecognizer(state: .Cancelled))) mockDelegate = MockGestureReactorDelegate() sut.delegate = mockDelegate }