Search code examples
iosswiftavfoundationuigesturerecognizeruipinchgesturerecognizer

Unproblematic use of UIPinchGestureRecognizer with UILongTapGestureRecognizer


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
}

Solution

  • 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.