More Gesture Recognizers

In this section, you are going to add the ability for a user to select a line by pressing and holding (a long press) and then move the selected line by dragging the finger (a pan). This will require two more subclasses of UIGestureRecognizer: UILongPressGestureRecognizer and UIPanGestureRecognizer.

UILongPressGestureRecognizer

In DrawView.swift, instantiate a UILongPressGestureRecognizer in init?(coder:) and add it to the DrawView.

    ...
    addGestureRecognizer(tapRecognizer)

    let longPressRecognizer = UILongPressGestureRecognizer(target: self,
            action: #selector(DrawView.longPress(_:)))
    addGestureRecognizer(longPressRecognizer)
}

Now when the user holds down on the DrawView, the method longPress(_:) will be called on it. By default, a touch must be held 0.5 seconds to become a long press, but you can change the minimumPressDuration of the gesture recognizer if you like.

So far, you have worked with tap gestures. A tap is a discrete gesture. By the time it is recognized, the gesture is over, and the action message has been delivered. A long press, on the other hand, is a continuous gesture. Continuous gestures occur over time. To keep track of what is going on with a continuous gesture, you can check a recognizer’s state property.

For example, consider a typical long press:

  • When the user touches a view, the long-press recognizer notices a possible long press, but it must wait to see whether the touch is held long enough to become a long-press gesture. The recognizer’s state is UIGestureRecognizerState.possible.

  • Once the user holds the touch long enough, the long press is recognized and the gesture has begun. The recognizer’s state is UIGestureRecognizerState.began.

  • When the user removes the finger, the gesture has ended. The recognizer’s state is UIGestureRecognizerState.ended.

When the long-press gesture recognizer transitions from possible to began and from began to ended, it sends its action message to its target. To determine which transition triggered the action, you check the gesture recognizer’s state.

Remember that the long press is part of a larger feature. In the next section, you will enable the user to move the selected line by dragging it with the same finger that began the long press. So here is the plan for implementing the longPress(_:) action method: When the recognizer is in the began state, you will select the closest line to where the gesture occurred. When the recognizer is in the ended state, you will deselect the line.

In DrawView.swift, implement longPress(_:).

func longPress(_ gestureRecognizer: UIGestureRecognizer) {
    print("Recognized a long press")

    if gestureRecognizer.state == .began {
        let point = gestureRecognizer.location(in: self)
        selectedLineIndex = indexOfLine(at: point)

        if selectedLineIndex != nil {
            currentLines.removeAll()
        }
    } else if gestureRecognizer.state == .ended {
        selectedLineIndex = nil
    }

    setNeedsDisplay()
}

Build and run the application. Draw a line and then press and hold it; the line will turn green and become the selected line. When you let go, the line will revert to its former color and will no longer be the selected line.

UIPanGestureRecognizer and simultaneous recognizers

In DrawView.swift, declare a UIPanGestureRecognizer as a property so that you have access to it in all of your methods.

class DrawView: UIView {

    var currentLines = [NSValue:Line]()
    var finishedLines = [Line]()
    var selectedLineIndex: Int? {
        ...
    }
    var moveRecognizer: UIPanGestureRecognizer!

Next, in DrawView.swift, add code to init?(coder:) to instantiate a UIPanGestureRecognizer, set one of its properties, and add it to the DrawView.

    let longPressRecognizer = UILongPressGestureRecognizer(target: self,
            action: #selector(DrawView.longPress(_:)))
    addGestureRecognizer(longPressRecognizer)

    moveRecognizer = UIPanGestureRecognizer(target: self,
                                            action: #selector(DrawView.moveLine(_:)))
    moveRecognizer.cancelsTouchesInView = false
    addGestureRecognizer(moveRecognizer)
}

What is cancelsTouchesInView? Every UIGestureRecognizer has this property, which defaults to true. When cancelsTouchesInView is true, the gesture recognizer will eat any touch it recognizes, and the view will not get a chance to handle the touch via the traditional UIResponder methods, like touchesBegan(_:with:).

Usually, this is what you want, but not always. In this case, if the pan gesture recognizer were to eat its touches, then users would not be able to draw lines. When you set cancelsTouchesInView to false, you ensure that any touch recognized by the gesture recognizer will also be delivered to the view via the UIResponder methods.

In DrawView.swift, add a simple implementation for the action method:

func moveLine(_ gestureRecognizer: UIPanGestureRecognizer) {
    print("Recognized a pan")
}

Build and run the app and draw some lines. Because cancelsTouchesInView is false, the pan gesture is recognized, but lines can still be drawn. You can comment out the line that sets cancelsTouchesInView and run again to see the difference.

Soon, you will update moveLine(_:) to redraw the selected line as the user’s finger moves across the screen. But first you need two gesture recognizers to be able to handle the same touch. Normally, when a gesture recognizer recognizes its gesture, it eats it and no other recognizer gets a chance to handle that touch. Try it: Run the app, draw a line, press and hold to select the line, and then move your finger around. The console reports the long press but not the pan.

In this case, the default behavior is problematic: Your users will press and hold to select a line and then pan to move the line – without lifting the finger in between. Thus, the two gestures will occur simultaneously, and the pan gesture recognizer must be allowed to recognize a pan even though the long-press gesture has already recognized a long press.

To allow a gesture recognizer to recognize its gesture simultaneously with another gesture recognizer, you implement a method from the UIGestureRecognizerDelegate protocol:

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

The first parameter is the gesture recognizer that is asking for guidance. It says to its delegate, “So there’s me and this other recognizer, and one of us just recognized a gesture. Should the one who did not recognize it stay in the possible state and continue to track this touch?”

Note that the call itself does not tell you which of the two recognizers has recognized its gesture – and, thus, which of them will potentially be deprived of the chance to recognize its gesture.

By default, the method returns false, and the gesture recognizer still in the possible state leaves the touch in the hands of the gesture already in the recognized state. You can implement the method to return true to allow both recognizers to recognize their gestures in the same touch. (If you need to determine which of the two recognizers has recognized its gesture, you can check the recognizers’ state properties.)

To enable panning while long pressing, you are going to give the pan gesture recognizer a delegate (the DrawView). Then, when the long-press recognizer recognizes its gesture, the pan gesture recognizer will call the simultaneous recognition method on its delegate. You will implement this method in DrawView to return true. This will allow the pan gesture recognizer to recognize any panning that occurs while a long press is in progress.

First, in DrawView.swift, declare that DrawView conforms to the UIGestureRecognizerDelegate protocol.

class DrawView: UIView, UIGestureRecognizerDelegate {

    var currentLines = [NSValue:Line]()
    var finishedLines = [Line]()
    var selectedLineIndex: Int? {
        ...
    }
    var moveRecognizer: UIPanGestureRecognizer!

Next, in init?(coder:), set the DrawView to be the delegate of the UIPanGestureRecognizer.

    let longPressRecognizer = UILongPressGestureRecognizer(target: self,
            action: #selector(DrawView.longPress(_:)))
    addGestureRecognizer(longPressRecognizer)

    moveRecognizer = UIPanGestureRecognizer(target: self,
                                            action: #selector(DrawView.moveLine(_:)))
    moveRecognizer.delegate = self
    moveRecognizer.cancelsTouchesInView = false
    addGestureRecognizer(moveRecognizer)
}

Finally, in DrawView.swift, implement the delegate method to return true.

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

For this situation, where only your pan gesture recognizer has a delegate, there is no need to do more than return true. In more complicated scenarios, you would use the passed-in gesture recognizers to more carefully control simultaneous recognition.

Now, when a long press begins, the UIPanGestureRecognizer will continue to keep track of the touch, and if the user’s finger begins to move, the pan recognizer will recognize the pan. To see the difference, run the app, draw a line, select it, and then pan. The console will report both gestures.

(The UIGestureRecognizerDelegate protocol includes other methods to help you tweak the behavior of your gesture recognizers. Visit the protocol reference page for more information.)

In addition to the states you have already seen, a pan gesture recognizer supports the changed state. When a finger starts to move, the pan recognizer enters the began state and calls a method on its target. While the finger moves around the screen, the recognizer transitions to the changed state and calls the action method on its target repeatedly. When the finger leaves the screen, the recognizer’s state is set to ended, and the method is called on the target for the final time.

The next step is to implement the moveLine(_:) method that the pan recognizer calls on its target. In this implementation, you will call the method translationInView(_:) on the pan recognizer. This UIPanGestureRecognizer method returns how far the pan has moved as a CGPoint in the coordinate system of the view passed as the argument. When the pan gesture begins, this property is set to the zero point (where x and y are 0). As the pan moves, this value is updated – if the pan goes far to the right, it has a high x value; if the pan returns to where it began, its translation goes back to the zero point.

In DrawView.swift, implement moveLine(_:). Notice that because you will send the gesture recognizer a method from the UIPanGestureRecognizer class, the parameter of this method must be a reference to an instance of UIPanGestureRecognizer rather than UIGestureRecognizer.

func moveLine(_ gestureRecognizer: UIPanGestureRecognizer) {
    print("Recognized a pan")

    // If a line is selected...
    if let index = selectedLineIndex {
        // When the pan recognizer changes its position...
        if gestureRecognizer.state == .changed {
            // How far has the pan moved?
            let translation = gestureRecognizer.translation(in: self)

            // Add the translation to the current beginning and end points of the line
            // Make sure there are no copy and paste typos!
            finishedLines[index].begin.x += translation.x
            finishedLines[index].begin.y += translation.y
            finishedLines[index].end.x += translation.x
            finishedLines[index].end.y += translation.y

            // Redraw the screen
            setNeedsDisplay()
        }
    } else {
        // If no line is selected, do not do anything
        return
    }
}

Build and run the application. Touch and hold on a line and begin dragging – and you will immediately notice that the line and your finger are way out of sync. What is going on?

You are adding the current translation over and over again to the line’s original end points. You really need the gesture recognizer to report the change in translation since the last time this method was called instead. Fortunately, you can do this. You can set the translation of a pan gesture recognizer back to the zero point every time it reports a change. Then, the next time it reports a change, it will have the translation since the last event.

Near the bottom of moveLine(_:) in DrawView.swift, add the following line of code.

finishedLines[index].end.x += translation.x
finishedLines[index].end.y += translation.y

gestureRecognizer.setTranslation(CGPoint.zero, in: self)

// Redraw the screen
setNeedsDisplay()

Build and run the application and move a line around. Works great!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset