Diference between 2 GestureRecognizer - ios

I have 2 GesturerRecognizers, one is a custom 3DTouch and another one is the single UITapGestureRegognizer. I'd like to make that if you tap once it performs a segue and if you go trought the 3dtouch one you get another view. I have tried lot of things but it is not working (i have tried with timer even, but always is 0 at start, no matters if its 3dtouch or not.) That my 3dtouch custom implementation:
//Without this import line, you'll get compiler errors when implementing your touch methods since they aren't part of the UIGestureRecognizer superclass
import UIKit.UIGestureRecognizerSubclass
//Since 3D Touch isn't available before iOS 9, we can use the availability APIs to ensure no one uses this class for earlier versions of the OS.
#available(iOS 9.0, *)
public class ForceTouchGestureRecognizer: UIGestureRecognizer {
var timer = NSTimer()
var counter = Double()
//Since it also doesn't make sense to have our force variable as a settable property, I'm using a private instance variable to make our public force property read-only
private var _force: CGFloat = 0.0
//Because we don't know what the maximum force will always be for a UITouch, the force property here will be normalized to a value between 0.0 and 1.0.
public var force: CGFloat { get { return _force } }
public var maximumForce: CGFloat = 4.0
func timerAction() {
counter += 1
}
override public func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent) {
super.touchesBegan(touches, withEvent: event)
counter = 0
timer = NSTimer.scheduledTimerWithTimeInterval(0.5, target: self, selector: #selector(timerAction), userInfo: nil, repeats: true)
print("COUNTER: \(counter)")
normalizeForceAndFireEvent(.Began, touches: touches)
}
override public func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent) {
super.touchesMoved(touches, withEvent: event)
normalizeForceAndFireEvent(.Changed, touches: touches)
}
override public func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent) {
super.touchesEnded(touches, withEvent: event)
print("COUNTER: \(counter)")
normalizeForceAndFireEvent(.Ended, touches: touches)
timer.invalidate()
}
override public func touchesCancelled(touches: Set<UITouch>, withEvent event: UIEvent) {
super.touchesCancelled(touches, withEvent: event)
normalizeForceAndFireEvent(.Cancelled, touches: touches)
}
func normalizeForceAndFireEvent(state: UIGestureRecognizerState, touches: Set<UITouch>) {
//Putting a guard statement here to make sure we don't fire off our target's selector event if a touch doesn't exist to begin with.
guard let firstTouch = touches.first else {
return
}
//Just in case the developer set a maximumForce that is higher than the touch's maximumPossibleForce, I'm setting the maximumForce to the lower of the two values.
maximumForce = min(firstTouch.maximumPossibleForce, maximumForce)
//Now that I have a proper maximumForce, I'm going to use that and normalize it so the developer can use a value between 0.0 and 1.0.
_force = firstTouch.force / maximumForce
//Our properties are now ready for inspection by the developer. By setting the UIGestureRecognizer's state property, the system will automatically send the target the selector message that this recognizer was initialized with.
self.state = state
}
//This function is called automatically by UIGestureRecognizer when our state is set to .Ended. We want to use this function to reset our internal state.
public override func reset() {
super.reset()
_force = 0.0
}
}
And that is what i do in the view:
self.forceTouchRecognizer = ForceTouchGestureRecognizer(target: self, action: #selector(PLView.handleForceTouchGesture(_:)))
self.addGestureRecognizer(forceTouchRecognizer)
self.nowTap = UITapGestureRecognizer(target: self, action: #selector(StoryTableViewController.didTapRightNow2(_:)))
self.addGestureRecognizer(nowTap)

If I understand you correctly you want to add 2 different GestureRecognizers to the same UIView. One that detects a normal tap and one that detects a tap with force.
For the normal tap you can use a UITapGestureRecognizer. For the force tap you have to create your own custom Gesture Recognizer that monitors the force of the touch decides if the force was high enough to qualify for a force tap gesture.
Here is the custom force tap gesture recognizer:
import UIKit.UIGestureRecognizerSubclass
#available(iOS 9.0, *)
public class ForceTapGestureRecognizer: UIGestureRecognizer {
private var forceTapRecognized = false
override public func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent) {
super.touchesBegan(touches, withEvent: event)
forceTapRecognized = false
}
override public func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent) {
super.touchesMoved(touches, withEvent: event)
guard let touch = touches.first else { return }
if touch.force >= touch.maximumPossibleForce {
forceTapRecognized = true
}
}
override public func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent) {
super.touchesEnded(touches, withEvent: event)
state = forceTapRecognized ? .Ended : .Failed
}
}
Then you can add both recognizers to your view and add an action for each recognizer.
To make both recognizers work at the same time you have to tell the UITapGestureRecognizer that it should only detect a tap, when the ForceTapGestureRecognizer did not detect a force tap. You do this with requireGestureRecognizerToFail(_:). If you do not set this, only the normal taps will be recognized:
class ViewController: UIViewController {
let touchView = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
override func viewDidLoad() {
super.viewDidLoad()
let touchView = UIView()
view.addSubview(touchView)
let forceTapRecognizer = ForceTapGestureRecognizer(target: self, action: #selector(ViewController.didForceTap(_:)))
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(ViewController.didTap(_:)))
tapRecognizer.requireGestureRecognizerToFail(forceTapRecognizer)
touchView.addGestureRecognizer(forceTapRecognizer)
touchView.addGestureRecognizer(tapRecognizer)
}
func didTap(recognizer: UITapGestureRecognizer) {
print("tap")
}
func didForceTap(recognizer: ForceTapGestureRecognizer) {
print("force tap")
}
}

Related

Get touch type in hitTest() or pointInside()

I'm implementing a passThroughView by creating a transparent View on top and override hitTest().
passThroughView should consume touches from Apple Pencil and if touch type is not from pencil, it pass touches to the view underneath.
The problems are:
parameter "event" in hitTest contains no touch, so I can't check touch type in hitTest
I can get touch from touchesBegan and check touch type, but it get called only after hitTest returned true
I subclass UIWindow and override sendEvent() but this function also called after hitTest (and I don't know why)
class WindowAbleToKnowTouchTypes: UIWindow {
override func sendEvent(_ event: UIEvent) {
if event.type == .touches {
// This get called after Hittest
if event.allTouches!.first!.type == .pencil {
print("This touch is from Apple Pencil")
}
}
super.sendEvent(event)
}
}
Is there anyway to check touchType to decide to pass or consume touches?
I ended up using a different approach, it could be useful for many cases: If I can't get touchType in hitTest(), I can still get the touchType with GestureRecognize:
class CustomGestureRecognizer : ImmediatePanGesture {
var beganTouch : UITouch!
var movedTouch : UITouch!
var endedTouch : UITouch!
override func shouldReceive(_ event: UIEvent) -> Bool {
// You can check for touchType here and decide if this gesture should receice the touch or not
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
// Save touch for later use
if let firstTouch = touches.first {
beganTouch = firstTouch
}
super.touchesBegan(touches, with: event)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
// Save touch for later use
if let touch = touches.first {
movedTouch = touch
}
super.touchesMoved(touches, with: event)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
// Save touch for later use
if let touch = touches.first {
endedTouch = touch
}
super.touchesEnded(touches, with: event)
}
}
In the target function of the gestureRecognizer, you can get the UITouch by:
let beganTouch = customGesture.beganTouch
let touchType = beganTouch.touchType

Detecting UITouches while UIViewController is being shown as 3DTouch preview

Is it possible to detect touches and get the location of a touch from a UIViewController which is being currently used as previewingContext view controller for 3D Touch? (I want to change the image of within the preview controller when the touch moves from left to right)
I've tried both touchesBegan and touchesMoved none of them are fired.
class ThreeDTouchPreviewController: UIViewController {
func getLocationFromTouch(touches: Set<UITouch>) -> CGPoint?{
guard let touch = touches.first else { return nil }
return touch.location(in: self.view)
}
//Not fired
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let location = getLocationFromTouch(touches: touches)
print("LOCATION", location)
}
//Not fired
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
let location = getLocationFromTouch(touches: touches)
print("LOCATION", location)
}
}
Even tried adding a UIPanGesture.
Attempting to replicate FaceBook's 3D Touch feature where a user can move finger from left to right to change the current image being displayed.
Video for context: https://streamable.com/ilnln
If you want to achive result same as in FaceBook's 3D Touch feature you need to create your own 3D Touch gesture class
import UIKit.UIGestureRecognizerSubclass
class ForceTouchGestureRecognizer: UIGestureRecognizer {
var forceValue: CGFloat = 0
var isForceTouch: Bool = false
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
handleForceWithTouches(touches: touches)
state = .began
self.isForceTouch = false
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesMoved(touches, with: event)
handleForceWithTouches(touches: touches)
if self.forceValue > 6.0 {
state = .changed
self.isForceTouch = true
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesEnded(touches, with: event)
state = .ended
handleForceWithTouches(touches: touches)
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesCancelled(touches, with: event)
state = .cancelled
handleForceWithTouches(touches: touches)
}
func handleForceWithTouches(touches: Set<UITouch>) {
if touches.count != 1 {
state = .failed
return
}
guard let touch = touches.first else {
state = .failed
return
}
forceValue = touch.force
}
}
and now you can add this gesture in your ViewController in viewDidLoad method
override func viewDidLoad() {
super.viewDidLoad()
let gesture = ForceTouchGestureRecognizer(target: self, action: #selector(imagePressed(sender:)))
self.view.addGestureRecognizer(gesture)
}
Now you can manage your controller UI in Storyboard. Add cover view above UICollectionView and UIImageView in a center and connect it with IBOutlets in code.
Now you can add handler methods for gesture
func imagePressed(sender: ForceTouchGestureRecognizer) {
let location = sender.location(in: self.view)
guard let indexPath = collectionView?.indexPathForItem(
at: location) else { return }
let image = self.images[indexPath.row]
switch sender.state {
case .changed:
if sender.isForceTouch {
self.coverView?.isHidden = false
self.selectedImageView?.isHidden = false
self.selectedImageView?.image = image
}
case .ended:
print("force: \(sender.forceValue)")
if sender.isForceTouch {
self.coverView?.isHidden = true
self.selectedImageView?.isHidden = true
self.selectedImageView?.image = nil
} else {
//TODO: handle selecting items of UICollectionView here,
//you can refer to this SO question for more info: https://stackoverflow.com/questions/42372609/collectionview-didnt-call-didselectitematindexpath-when-superview-has-gesture
print("Did select row at indexPath: \(indexPath)")
self.collectionView?.selectItem(at: indexPath, animated: true, scrollPosition: .centeredVertically)
}
default: break
}
}
From this point you need to customize your view to make it look in same way as Facebook do.
Also I created small example project on GitHub https://github.com/ChernyshenkoTaras/3DTouchExample to demonstrate it

3D Touch force gesture activates cell tap on touch end

I have subclassed a UITableView in my app so that I can intercept touch events. I am using this to allow me to provide 3D Touch gestures on the entire view (including on top of the table view).
This works great, however the problem is that using 3D Touch on one of the cells and then releasing your finger activates the cell tap.
I need to only activate the cell tap if there is no force exerted. I should explain that I am fading an image in gradually over the entire screen as you apply pressure.
Here is my subclass:
protocol PassTouchesTableViewDelegate {
func touchMoved(touches: Set<UITouch>)
func touchEnded(touches: Set<UITouch>)
}
class PassTouchesTableView: UITableView {
var delegatePass: PassTouchesTableViewDelegate?
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
super.touchesMoved(touches, withEvent: event)
self.delegatePass?.touchMoved(touches)
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
super.touchesEnded(touches, withEvent: event)
self.delegatePass?.touchEnded(touches)
}
}
And here are the methods I'm calling from my view controller when the touches end and move:
internal func touchMoved(touches: Set<UITouch>) {
let touch = touches.first
self.pressureImageView.alpha = (touch!.force / 2) - 1.0
}
internal func touchEnded(touches: Set<UITouch>) {
UIView.animateWithDuration(0.2, animations: {
self.pressureImageView.alpha = 0.0
})
}
You could create a boolean named isForceTouch which is set to false in touchesBegan, and then once force touch is detected, set it to true. Then in didSelectRowAtIndexPath just return false if isForceTouch is true. It may need tweaking but that should work.

how i can differentiate between a gesture and a touch in spriteKit and swift?

i have two functions, one is activated when i touch the scene and another one when i make a gesture down, but when i make a gesture the scene detect a touch and execute both functions
class GameScene: SKScene, SKPhysicsContactDelegate {
override func didMoveToView(view: SKView) {
/* Setup your scene here */
// physics & collisions
physicsWorld.gravity = CGVectorMake(0.0, 0.0)
physicsWorld.contactDelegate = self
// Swipe down
let swipeDown:UISwipeGestureRecognizer = UISwipeGestureRecognizer(target: self, action: Selector("swipedDown:"))
swipeDown.direction = .Down
view.addGestureRecognizer(swipeDown)
}
//MARK: Swipes
func swipedDown(sender:UISwipeGestureRecognizer){
//first function
swipeDown()
}
//MARK: Touches
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
/* Called when a touch begins */
for touch in touches {
let location = touch.locationInNode(self)
// second function
attack()
}
}
}
Instead of touchesBegan, try using a tap gesture (UITapGestureRecognizer) for attack action.
This should not be necessary but if you still get conflict between swipe and tap, make the tap gesture depend on swipe's failure using requireGestureRecognizerToFail to ensure that a tap is not detected during a swipe.
You could try something like this
var swiped = Bool()
func swipedDown(sender:UISwipeGestureRecognizer){
//first function
swipeDown()
swiped = true
}
Override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
/* Called when a touch begins */
for touch in touches {
let location = touch.locationInNode(self)
// second function
if swiped == false {
attack()
}
}
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
for touch in touches {
swiped = false
}
}
Hope this helps.

SpriteKit custom UIScrollView with acceleration

Basically I need to create a UIScrollView in my SpriteKit project but i'm having a lot of problem adding SKButtons (custom SKNode class for button management). So I proceeded to create a scrollable SKNode with touch gesture but, obviously, this bar won't have the native UIScrollView acceleration: the feature I was looking for.
So tried to overtake this problem by adding a native UIScrollView and catch every change of position, like this:
using this code:
-(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
[blueNode setPosition:CGPointMake(blueNode.position.x, scrollerUI.contentOffset)];
}
This works right but foolishly I forgot that if I add buttons, the touch gesture won't recognize the button's touch event! (The UIScrollView has the priority).
Maybe is just a stupid question but I don't really know how to figure it out. Maybe programming my own acceleration methods?
I think I came up with a solution to handle touches. Since the UIScrollView eats all the touches to allow it to scroll you need to pass them to the SKScene. You can do that by subclassing UIScrollView:
class SpriteScrollView: UIScrollView {
var scene: SKScene?
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
scene?.touchesBegan(touches, withEvent: event)
}
override func touchesMoved(touches: Set<NSObject>, withEvent event: UIEvent) {
scene?.touchesMoved(touches, withEvent: event)
}
override func touchesCancelled(touches: Set<NSObject>!, withEvent event: UIEvent!) {
scene?.touchesCancelled(touches, withEvent: event)
}
override func touchesEnded(touches: Set<NSObject>, withEvent event: UIEvent) {
scene?.touchesEnded(touches, withEvent: event)
}
}
Then your scene needs to see if the touch hits any nodes and appropriately send the touches to those nodes. So in your SKScene add this:
var nodesTouched: [AnyObject] = []
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
super.touchesBegan(touches, withEvent: event)
let location = (touches.first as! UITouch).locationInNode(self)
nodesTouched = nodesAtPoint(location)
for node in nodesTouched as! [SKNode] {
node.touchesBegan(touches, withEvent: event)
}
}
override func touchesMoved(touches: Set<NSObject>, withEvent event: UIEvent) {
super.touchesMoved(touches, withEvent: event)
for node in nodesTouched as! [SKNode] {
node.touchesMoved(touches, withEvent: event)
}
}
override func touchesCancelled(touches: Set<NSObject>!, withEvent event: UIEvent!) {
super.touchesCancelled(touches, withEvent: event)
for node in nodesTouched as! [SKNode] {
node.touchesCancelled(touches, withEvent: event)
}
}
override func touchesEnded(touches: Set<NSObject>, withEvent event: UIEvent) {
super.touchesEnded(touches, withEvent: event)
for node in nodesTouched as! [SKNode] {
node.touchesEnded(touches, withEvent: event)
}
}
Note this is for single touches and does not handle errors well, you'll need to wrap some of the forced down casting in if statements if you want to do this properly
I hope this helps someone even though the question is months old.
One possible solution would be to add tap gesture to scene:
override func didMoveToView(view: SKView) {
let tap : UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: Selector("tapped:"))
view.addGestureRecognizer(tap)
}
func tapped(tap : UITapGestureRecognizer) {
let viewLocation = tap.locationInView(tap.view)
let location = convertPointFromView(viewLocation)
let node = nodeAtPoint(location)
if node.name == "lvlButton" {
var button = node as! SKButton
button.touchBegan(location)
}
}

Resources