Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand All @@ -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 */

Expand Down Expand Up @@ -53,6 +56,10 @@
0C9FFB341C46625F00FD6C05 /* ReactiveViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactiveViewController.swift; sourceTree = "<group>"; };
0CDDAB471C56C21C00D5DA3D /* ReactiveShortViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactiveShortViewController.swift; sourceTree = "<group>"; };
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 = "<group>"; };
1BF29ED91C69E72D00912AD3 /* UIView+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+Extensions.swift"; sourceTree = "<group>"; };
1F5B23AB1C8EBA7B00F28DE1 /* IntegratedReactiveGestureReactor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IntegratedReactiveGestureReactor.swift; sourceTree = "<group>"; };
1F5B23AD1C8EBCAD00F28DE1 /* IntegratedReactiveViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IntegratedReactiveViewController.swift; sourceTree = "<group>"; };
1F5B23AF1C8EC62100F28DE1 /* IntegratedReactiveGestureReactorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IntegratedReactiveGestureReactorTests.swift; sourceTree = "<group>"; };
1FB180CB1C6C72EF008BC2D1 /* UIGestureRecognizerProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIGestureRecognizerProtocol.swift; sourceTree = "<group>"; };
1FD5DC421C69D4B60050B3D9 /* ImperativeGestureReactor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImperativeGestureReactor.swift; sourceTree = "<group>"; };
1FD5DC441C69D60C0050B3D9 /* GestureReactor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GestureReactor.swift; sourceTree = "<group>"; };
Expand All @@ -61,7 +68,6 @@
1FE3E4361C6CF6B800804CA2 /* ReactiveTimer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactiveTimer.swift; sourceTree = "<group>"; };
1FE3E4381C6DC6CB00804CA2 /* GestureReactorTestHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GestureReactorTestHelper.swift; sourceTree = "<group>"; };
1FE3E43A1C6E40A200804CA2 /* ReactiveGestureReactorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactiveGestureReactorTests.swift; sourceTree = "<group>"; };
1BF29ED91C69E72D00912AD3 /* UIView+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+Extensions.swift"; sourceTree = "<group>"; };
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 = "<group>"; };
AA3577BA56804CC07EC782AF /* Pods.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods.framework; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
Expand Down Expand Up @@ -124,6 +130,8 @@
1FB180CB1C6C72EF008BC2D1 /* UIGestureRecognizerProtocol.swift */,
1FE3E4341C6C782F00804CA2 /* TimerProtocol.swift */,
1FE3E4361C6CF6B800804CA2 /* ReactiveTimer.swift */,
1F5B23AB1C8EBA7B00F28DE1 /* IntegratedReactiveGestureReactor.swift */,
1F5B23AD1C8EBCAD00F28DE1 /* IntegratedReactiveViewController.swift */,
);
path = RFP;
sourceTree = "<group>";
Expand All @@ -135,6 +143,7 @@
0C18B47E1C4454580081DFC5 /* Info.plist */,
1FE3E4381C6DC6CB00804CA2 /* GestureReactorTestHelper.swift */,
1FE3E43A1C6E40A200804CA2 /* ReactiveGestureReactorTests.swift */,
1F5B23AF1C8EC62100F28DE1 /* IntegratedReactiveGestureReactorTests.swift */,
);
path = RFPTests;
sourceTree = "<group>";
Expand Down Expand Up @@ -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 */,
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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<UIGestureRecognizerType>, rotateGestureObservable: Observable<UIGestureRecognizerType>) {

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<UIGestureRecognizerState> in
return Observable.just(gesture.state)
}

let panGesturesStartedEnded = panGestureObservable.filter { gesture in gesture.state == .Began || gesture.state == .Ended}.flatMap { (gesture) -> Observable<UIGestureRecognizerState> 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<UIGestureRecognizerState> 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
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

somehow this is never called - strange, in ReactiveGestureReactor exactly the same implementation is used, and there this line is called just fine

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@itchingpixels @morgz have you any idea why this behaves like this? i really cannot explain/fix it.

}

// 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)

}

}
Original file line number Diff line number Diff line change
@@ -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<UIGestureRecognizer> to Observable<UIGestureRecognizerType>
let panObservable: Observable<UIGestureRecognizerType> = pan.rx_event.asObservable().flatMap { gesture -> Observable<UIGestureRecognizerType> in
return Observable.just(gesture as UIGestureRecognizerType)
}
let rotateObservable: Observable<UIGestureRecognizerType> = rotate.rx_event.asObservable().flatMap { gesture -> Observable<UIGestureRecognizerType> 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<UIGestureRecognizer> to Observable<UIGestureRecognizerType>
let panObservable: Observable<UIGestureRecognizerType> = pan.rx_event.asObservable().flatMap { gesture -> Observable<UIGestureRecognizerType> in
return Observable.just(gesture as UIGestureRecognizerType)
}
let rotateObservable: Observable<UIGestureRecognizerType> = rotate.rx_event.asObservable().flatMap { gesture -> Observable<UIGestureRecognizerType> 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
}

}
Loading