1

I need to use a UILongTapGestureRecognizer with a UIPinchGestureRecognizer simultaneously.

Unfortunately, the first touch of the UILongTapGestureRecognizer will be counted for the PinchGestureRecognizer too. So when holding UILongTapGestureRecognizer there just needs to be one more touch to trigger the Pinch Recognizer. One is used for long press gesture and two for the pinch.

Is there a way to use both independently? I do not want to use the touch of the UILongTapGestureRecognizer for my UIPinchGestureRecognizer.

This is how I enable my simultaneous workforce:

func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {

    //Allowing
    if (gestureRecognizer == zoom) && (otherGestureRecognizer == longTap) {
        print("working while filming")
        return true
    }

    return false
}
halfer
  • 19,824
  • 17
  • 99
  • 186
thelearner
  • 1,440
  • 3
  • 27
  • 58
  • Not 100% sure what you are asking... Are you saying you want user to use 3 fingers where one is used for long press gesture and another for pinch? – Matic Oblak Mar 09 '18 at 09:45
  • Thanks for the help, yes that is exactly what I need. – thelearner Mar 09 '18 at 09:50
  • 1
    The bottom line is that if you are using `UIGestureRecognizers` you are at the mercy of the operating system - in this case `UIKit`. There *may* be a way to intercept that first tap - but your best best is to go "deeper" and just use touch events instead of gestures. (You'll be glad you did.) A bit of possible criticism? Why are you asking the user to do this (a long press/tap followed by a pinch)? Most (as in virtually all) apps don't ask this of their users. You might see how other apps deal with the functionality you are seeking and copy that. Either way, use the touch events. Good luck! –  Mar 09 '18 at 09:52

1 Answers1

2

I don't believe you have the tool for what you are looking for so I suggest you try to create your own gesture recognizer. It is not really that hard but you will unfortunately need to do both the long press and the pinch effect.

Don't try overriding UIPinchGestureRecognizer nor UILongPressGestureRecognizer because it will simply not work (or if you mange it please do share your findings). So just go straight for UIGestureRecognizer.

So to begin with long press gesture recognizer we need to track that user presses and holds down for long enough time without moving too much. So we have:

var minimumPressDuration = UILongPressGestureRecognizer().minimumPressDuration
var allowableMovement = UILongPressGestureRecognizer().allowableMovement

Now touches need to be overridden (this is all in subclass of gesture recognizer):

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
    touchMoveDistance = 0.0 // Reset the movement to zero
    previousLocation = touches.first?.location(in: view) // We need to save the previous location
    longPressTimer = Timer.scheduledTimer(timeInterval: minimumPressDuration, target: self, selector: #selector(onTimer), userInfo: nil, repeats: false) // Initiate a none-repeating timer
    if inProgress == false { // inProgress will return true when stati is either .began or .changed
        super.touchesBegan(touches, with: event)
    }
}

override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
    if let newLocation = touches.first?.location(in: view), let previousLocation = previousLocation {
        self.previousLocation = newLocation
        touchMoveDistance += abs(previousLocation.y-newLocation.y) + abs(previousLocation.x-newLocation.x) // Don't worry about precision of this. We can't know the path between the 2 points anyway
    }
    if inProgress == false {
        super.touchesMoved(touches, with: event)
    }
}

override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
    longPressTimer?.invalidate()
    longPressTimer = nil
    if inProgress {
        state = .ended
    }
    super.touchesEnded(touches, with: event)

    if self.isEnabled { // This will simply reset the gesture
        self.isEnabled = false
        self.isEnabled = true
    }
}

override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
    longPressTimer?.invalidate()
    longPressTimer = nil
    if inProgress {
        state = .ended
    }
    super.touchesCancelled(touches, with: event)

    if self.isEnabled {
        self.isEnabled = false
        self.isEnabled = true
    }
}

So these are all only for long press. And what happens on timer is:

@objc private func onTimer() {
    longPressTimer?.invalidate()
    longPressTimer = nil
    if state == .possible {
        state = .began
    }
}

So basically if we change state to .begin we trigger the gesture and rest of the events simply work. Which is pretty neat.

Unfortunately this is far from over and you will need to play around a bit with the rest of the code...

You will need to preserve touches (If I remember correctly the same touch will be reported as a comparable object until user lifts his finger):

  1. On begin save the touch to a private property longPressTouch
  2. On timer when long press succeeds set a property to indicate the long press has indeed triggered didSucceedLongPress = true
  3. On begin check if another touch can be added and cancel gesture if it may not if longPressTouch != nil && didSucceedLongPress == false { cancel() }. Or allow it, this really depends on what you want.
  4. On begin if touches may be added then add them in array and save their initial positions pinchTouches.append((touch: touch, initialPosition: touchPosition))
  5. On touches end and cancel make sure to remove appropriate touches from array. And if long press is removed cancel the event (or not, your choice again)

So this should be all the data you need for your pinch gesture recognizer. Since all the events should already be triggering for you the way you need it all you need is a computed value for your scale:

var pinchScale: CGFloat {
    guard didSucceedLongPress, pinchTouches.count >= 2 else {
        return 1.0 // Not having enough data yet
    }
    return distanceBetween(pinchTouches[0].touch, pinchTouches[1].touch)/distanceBetween(pinchTouches[0].initialPosition, pinchTouches[1].initialPosition) // Shouldn't be too hard to make
}

Then there are some edge cases you need to check like: user initiates a long press, uses 2 fingers to pinch, adds 3rd finger (ignored), removes 2nd finger: Without handling this you might get a little jump which may or may not be intended. I guess you could just cancel the gesture or you could somehow transform the initial values to make the jump disappear.

So good luck if you will be implementing this and let us know how it went.

Matic Oblak
  • 16,318
  • 3
  • 24
  • 43
  • impressive answer! Thanks a lot for helping me. I really wonder, how long did it take you to acquire this knowledge? – thelearner Mar 09 '18 at 17:01
  • 1
    @JohannaNoobie Basically just luck... I have a gesture recognizer that combines long press and force touch which is what is reduced to the first part. As for the pinch and handling touches manually I have some memory from the days where gesture recognizers were not yet a thing and we had to do them manually. – Matic Oblak Mar 12 '18 at 08:39