Multiple Gesture Recognizers

The next step is to add another gesture recognizer that allows the user to select a line. (Later, a user will be able to delete the selected line.) You will install another UITapGestureRecognizer on the DrawView that only requires one tap.

In DrawView.swift, modify init?(coder:) to add this gesture recognizer.

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)

    let doubleTapRecognizer = UITapGestureRecognizer(target: self,
        action: #selector(DrawView.doubleTap(_:)))
    doubleTapRecognizer.numberOfTapsRequired = 2
    doubleTapRecognizer.delaysTouchesBegan = true
    addGestureRecognizer(doubleTapRecognizer)

    let tapRecognizer =
        UITapGestureRecognizer(target: self, action: #selector(DrawView.tap(_:)))
    tapRecognizer.delaysTouchesBegan = true
    addGestureRecognizer(tapRecognizer)
}

Now, implement tap(_:) in DrawView.swift to log the tap to the console.

func tap(_ gestureRecognizer: UIGestureRecognizer) {
    print("Recognized a tap")
}

Build and run the application. Try tapping and double-tapping. Tapping once logs the appropriate message to the console. Double-tapping, however, triggers both tap(_:) and doubleTap(_:).

In situations where you have multiple gesture recognizers, it is not uncommon to have one gesture recognizer fire and claim a touch when you really wanted another gesture recognizer to handle it. In these cases, you set up dependencies between recognizers that say, Wait a moment before you fire, because this touch might be mine!

In init?(coder:), make it so the tapRecognizer waits until the doubleTapRecognizer fails to recognize a double-tap before claiming the single tap for itself.

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)

    let doubleTapRecognizer = UITapGestureRecognizer(target: self,
        action: #selector(DrawView.doubleTap(_:)))
    doubleTapRecognizer.numberOfTapsRequired = 2
    doubleTapRecognizer.delaysTouchesBegan = true
    addGestureRecognizer(doubleTapRecognizer)

    let tapRecognizer =
        UITapGestureRecognizer(target: self, action: #selector(DrawView.tap(_:)))
    tapRecognizer.delaysTouchesBegan = true
    tapRecognizer.require(toFail: doubleTapRecognizer)
    addGestureRecognizer(tapRecognizer)
}

Build and run the application again and try out some taps. A single tap now takes a small amount of time to fire after the tap occurs, but double-tapping no longer triggers the tap(_:) message.

Next, let’s build on the DrawView so that the user can select a line when it is tapped. First, add a property at the top of DrawView.swift to hold on to the index of a selected line.

class DrawView: UIView {

    var currentLines = [NSValue:Line]()
    var finishedLines = [Line]()
    var selectedLineIndex: Int?

Now modify draw(_:) to draw the selected line in green.

override func draw(_ rect: CGRect) {
    finishedLineColor.setStroke()
    for line in finishedLines {
        stroke(line)
    }

    currentLineColor.setStroke()
    for (_,line) in currentLines {
        stroke(line)
    }

    if let index = selectedLineIndex {
        UIColor.green.setStroke()
        let selectedLine = finishedLines[index]
        stroke(selectedLine)
    }
}

Still in DrawView.swift, add an indexOfLine(at:) method that returns the index of the Line closest to a given point.

func indexOfLine(at point: CGPoint) -> Int? {
    // Find a line close to point
    for (index, line) in finishedLines.enumerated() {
        let begin = line.begin
        let end = line.end

        // Check a few points on the line
        for t in stride(from: CGFloat(0), to: 1.0, by: 0.05) {
            let x = begin.x + ((end.x - begin.x) * t)
            let y = begin.y + ((end.y - begin.y) * t)

            // If the tapped point is within 20 points, let's return this line
            if hypot(x - point.x, y - point.y) < 20.0 {
                return index
            }
        }
    }

    // If nothing is close enough to the tapped point, then we did not select a line
    return nil
}

The stride(from:to:by:) method will allow t to start at the from value and go up to (but not reach) the to value, incrementing the value of t by the by value.

There are other, better ways to determine the closest line to a point, but this simple implementation will work for your purposes.

The point to be passed in is the point where the tap occurred. You can easily get this information. Every UIGestureRecognizer has a location(in:) method. Calling this method on the gesture recognizer will give you the coordinate where the gesture occurred in the coordinate system of the view that is passed as the argument.

In DrawView.swift, update tap(_:) to call location(in:) on the gesture recognizer, pass the result to indexOfLine(at:), and make the returned index the selectedLineIndex.

func tap(_ gestureRecognizer: UIGestureRecognizer) {
    print("Recognized a tap")

    let point = gestureRecognizer.location(in: self)
    selectedLineIndex = indexOfLine(at: point)

    setNeedsDisplay()
}

If the user double-taps to clear all lines while a line is selected, the application will trap. To address this, update doubleTap(_:) to set the selectedLineIndex to nil.

func doubleTap(_ gestureRecognizer: UIGestureRecognizer) {
    print("Recognized a double tap")

    selectedLineIndex = nil
    currentLines.removeAll()
    finishedLines.removeAll()
    setNeedsDisplay()
}

Build and run the application. Draw a few lines and then tap one. The tapped line should appear in green (remember that it takes a moment before the tap is known not to be a double-tap).

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

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