Chapter 5. Touches

[Winifred the Woebegone illustrates hit-testing:] Hey nonny nonny, is it you? — Hey nonny nonny nonny no! — Hey nonny nonny, is it you? — Hey nonny nonny nonny no!

Marshall Barer, Once Upon a Mattress

A touch is an instance of the user putting a finger on the screen. The system and the hardware, working together, know when a finger contacts the screen and where it is. A finger is fat, but its location is cleverly reduced to a single point.

A UIResponder is a potential recipient of touches. A UIView is a UIResponder, and is thus the visible recipient of touches. There are other UIResponder subclasses, but none of them is visible on the screen. The user sees a view by virtue of its underlying layer; the user touches a view by virtue of the fact that it is a UIResponder.

A touch is represented as an object (a UITouch instance) which is bundled up in an envelope (a UIEvent) which the system delivers to your app. It is then up to your app to deliver the envelope to the appropriate UIView. In the vast majority of cases, this will happen automatically the way you expect, and you will respond to a touch by way of the view in which the touch occurred.

In fact, usually you won’t concern yourself with UIEvents and UITouches at all. Most built-in interface views deal with these low-level touch reports themselves, and notify your code at a higher level — you hear about functionality and intention rather than raw touches. When a UIButton emits an action message to report a Touch Up Inside control event, it has already performed a reduction of a complex sequence of touches; but what the button reports to you is simply that it was tapped. Similarly, a UITextField reports touches on the keyboard as changes in its own text. A UITableView reports that the user selected a cell. A UIScrollView, when dragged, reports that it scrolled; when pinched outward, it reports that it zoomed.

Nevertheless, it is useful to know how to respond to touches directly, so that you can implement your own touchable views, and so that you understand what Cocoa’s built-in views are actually doing. In this chapter, I’ll start by discussing touch detection and response by views (and other UIResponders) at their lowest level, along with a higher-level, more practical mechanism, gesture recognizers, that categorizes touches into gesture types for you. Then I’ll deconstruct the touch-delivery architecture by which touches are reported to your views in the first place.

Touch Events and Views

Imagine a screen that the user is not touching at all: the screen is “finger-free.” Now the user touches the screen with one or more fingers. From that moment until the time the screen is once again finger-free, all touches and finger movements together constitute what Apple calls a single multitouch sequence.

The system reports to your app, during a given multitouch sequence, every change in finger configuration, so that your app can figure out what the user is doing. Every such report is a UIEvent. In fact, every report having to do with the same multitouch sequence is the same UIEvent instance, arriving repeatedly, each time there’s a change in finger configuration.

Every UIEvent reporting a change in the user’s finger configuration contains one or more UITouch objects. Each UITouch object corresponds to a single finger; conversely, every finger touching the screen is represented in the UIEvent by a UITouch object. Once a UITouch instance has been created to represent a finger that has touched the screen, the same UITouch instance is used to represent that finger throughout this multitouch sequence until the finger leaves the screen.

Now, it might sound as if the system, during a multitouch sequence, constantly has to bombard the app with huge numbers of reports. But that’s not really true. The system needs to report only changes in the finger configuration. For a given UITouch object (representing, remember, a specific finger), only four things can happen. These are called touch phases, and are described by a UITouch instance’s phase property (UITouchPhase):

.began

The finger touched the screen for the first time; this UITouch instance has just been created. This is always the first phase, and arrives only once.

.moved

The finger moved upon the screen.

.stationary

The finger remained on the screen without moving. Why is it necessary to report this? Well, remember, once a UITouch instance has been created, it must be present every time the UIEvent for this multitouch sequence arrives. So if the UIEvent arrives because something else happened (e.g., a new finger touched the screen), we must report what this finger has been doing, even if it has been doing nothing.

.ended

The finger left the screen. Like .began, this phase arrives only once. The UITouch instance will now be destroyed and will no longer appear in UIEvents for this multitouch sequence.

Those four phases are sufficient to describe everything that a finger can do. Actually, there is one more possible phase:

.cancelled

The system has aborted this multitouch sequence because something interrupted it. What might interrupt a multitouch sequence? There are many possibilities. Perhaps the user clicked the Home button or the screen lock button in the middle of the sequence. A local notification alert may have appeared (Chapter 13); on an iPhone, a call may have come in. And as we shall see, a gesture recognizer recognizing its gesture may also trigger touch cancellation. The point is, if you’re dealing with touches yourself, you cannot afford to ignore touch cancellations; they are your opportunity to get things into a coherent state when the sequence is interrupted.

When a UITouch first appears (.began), your app works out which UIView it is associated with. (I’ll give full details, later in this chapter, as to how it does that.) This view is then set as the touch’s view property, and remains so; from then on, this UITouch is always associated with this view (until that finger leaves the screen).

The UITouches that constitute a UIEvent might be associated with different views. Accordingly, one and the same UIEvent is distributed to all the views of all the UITouches it contains. Conversely, if a view is sent a UIEvent, it’s because that UIEvent contains at least one UITouch whose view is this view.

If every UITouch in a UIEvent associated with a certain UIView has the phase .stationary, that UIEvent is not sent to that UIView. There’s no point, because as far as that view is concerned, nothing happened.

Warning

Do not retain a reference to a UITouch or UIEvent object over time; it is mutable and doesn’t belong to you. If you want to save touch information, extract and save the information, not the touch itself.

Receiving Touches

A UIResponder, and therefore a UIView, has four methods corresponding to the four UITouch phases that require UIEvent delivery. A UIEvent is delivered to a view by calling one of the four touch methods. Here they are:

touchesBegan(_:with:)

A finger touched the screen, creating a UITouch.

touchesMoved(_:with:)

A finger previously reported to this view with touchesBegan(_:with:) has moved. (On a device with 3D touch, “moved” might mean a change of pressure rather than location.)

touchesEnded(_:with:)

A finger previously reported to this view with touchesBegan(_:with:) has left the screen.

touchesCancelled(_:with:)

We are bailing out on a finger previously reported to this view with touchesBegan(_:with:).

The touch methods’ parameters are:

The relevant touches

These are the event’s touches whose phase corresponds to the name of the method and (normally) whose view is this view. They arrive as a Set. If there is only one touch in the set, or if any touch in the set will do, you can retrieve it with first (a set is unordered, so which element is first is arbitrary).

The event

This is the UIEvent instance. It contains its touches as a Set, which you can retrieve with the allTouches message. This means all the event’s touches, including but not necessarily limited to those in the first parameter; there might be touches in a different phase or intended for some other view. You can call touches(for:) to ask for the set of touches associated with a particular view or window.

So, when we say that a certain view is receiving a touch, that is a shorthand expression meaning that it is being sent a UIEvent containing this UITouch, over and over, by calling one of its touch methods, corresponding to the phase this touch is in, from the time the touch is created until the time it is destroyed.

A UITouch has some useful methods and properties:

location(in:), previousLocation(in:)

The current and previous location of this touch with respect to the coordinate system of a given view. The view you’ll be interested in will often be self or self.superview; supply nil to get the location with respect to the window. The previous location will be of interest only if the phase is .moved.

timestamp

When the touch last changed. A touch is timestamped when it is created (.began) and each time it moves (.moved). There can be a delay between the occurrence of a physical touch and the delivery of the corresponding UITouch, so to learn about the timing of touches, consult the timestamp, not the clock.

tapCount

If two touches are in roughly the same place in quick succession, and the first one is brief, the second one may be characterized as a repeat of the first. They are different touch objects, but the second will be assigned a tapCount one larger than the previous one. The default is 1, so if (for example) a touch’s tapCount is 3, then this is the third tap in quick succession in roughly the same spot.

view

The view with which this touch is associated.

majorRadius, majorRadiusTolerance

Respectively, the radius of the touch (approximately half its size) and the uncertainty of that measurement, in points.

(A UITouch carries some additional information that may be useful if the touch arrived through an Apple Pencil rather than a finger; for example, it describes how the pencil is oriented.)

Here are some additional UIEvent properties:

type

This will be UIEventType.touches. There are other event types, but you’re not going to receive any of them this way.

timestamp

When the event occurred.

Tip

Starting in iOS 9, you can reduce the latency between the user’s touches and your app’s rendering to the screen. On certain devices, the touch detection rate is doubled or even quadrupled, and you can ask for the extra touches. On all devices, a few future touches may be predicted, and you can ask for these. Such features would be useful, for example, in a drawing app.

Restricting Touches

Touch events can be turned off entirely at the application level with UIApplication’s beginIgnoringInteractionEvents. It is quite common to do this during animations and other lengthy operations during which responding to a touch could cause undesirable results. This call should be balanced by endIgnoringInteractionEvents. Pairs can be nested, in which case interactivity won’t be restored until the outermost endIgnoringInteractionEvents has been reached.

A number of UIView properties also restrict the delivery of touches to particular views:

isUserInteractionEnabled

If set to false, this view (along with its subviews) is excluded from receiving touches. Touches on this view or one of its subviews “fall through” to a view behind it.

alpha

If set to 0.0 (or extremely close to it), this view (along with its subviews) is excluded from receiving touches. Touches on this view or one of its subviews “fall through” to a view behind it.

isHidden

If set to true, this view (along with its subviews) is excluded from receiving touches. This makes sense, since from the user’s standpoint, the view and its subviews are not even present.

isMultipleTouchEnabled

If set to false, this view never receives more than one touch simultaneously; once it receives a touch, it doesn’t receive any other touches until that first touch has ended.

isExclusiveTouch

An isExclusiveTouch view receives a touch only if no other views in the same window have touches associated with them; once an isExclusiveTouch view has received a touch, then while that touch exists no other view in the same window receives any touches. (This is the only one of these properties that can’t be set in the nib editor.)

Interpreting Touches

Thanks to gesture recognizers (discussed later in this chapter), in most cases you won’t have to interpret touches at all; you’ll let a gesture recognizer do most of that work. Even so, it is beneficial to be conversant with the nature of touch interpretation; this will help you interact with a gesture recognizer, write your own gesture recognizer, or subclass an existing one. Furthermore, not every touch sequence can be codified through a gesture recognizer; sometimes, directly interpreting touches is the best approach.

To figure out what’s going on as touches are received by a view, your code must essentially function as a kind of state machine. You’ll receive various touch method calls, and your response will partly depend upon what happened previously, so you’ll have to record somehow, such as in instance properties, the information that you’ll need in order to decide what to do when the next touch method is called. Such an architecture can make writing and maintaining touch-analysis code quite tricky.

To illustrate the business of interpreting touches, we’ll start with a view that can be dragged with the user’s finger. For simplicity, I’ll assume that this view receives only a single touch at a time. (This assumption is easy to enforce by setting the view’s isMultipleTouchEnabled to false, which is the default.)

The trick to making a view follow the user’s finger is to realize that a view is positioned by its center, which is in superview coordinates, but the user’s finger might not be at the center of the view. So at every stage of the drag we must change the view’s center by the change in the user’s finger position in superview coordinates:

override func touchesMoved(_ touches: Set<UITouch>, with e: UIEvent?) {
    let t = touches.first!
    let loc = t.location(in:self.superview)
    let oldP = t.previousLocation(in:self.superview)
    let deltaX = loc.x - oldP.x
    let deltaY = loc.y - oldP.y
    var c = self.center
    c.x += deltaX
    c.y += deltaY
    self.center = c
}

Next, let’s add a restriction that the view can be dragged only vertically or horizontally. All we have to do is hold one coordinate steady; but which coordinate? Everything seems to depend on what the user does initially. So we’ll do a one-time test the first time we receive touchesMoved(_:with:). Now we’re maintaining two Bool state properties, self.decided and self.horiz:

override func touchesBegan(_ touches: Set<UITouch>, with e: UIEvent?) {
    self.decided = false
}
override func touchesMoved(_ touches: Set<UITouch>, with e: UIEvent?) {
    let t = touches.first!
    if !self.decided {
        self.decided = true
        let then = t.previousLocation(in:self)
        let now = t.location(in:self)
        let deltaX = abs(then.x - now.x)
        let deltaY = abs(then.y - now.y)
        self.horiz = deltaX >= deltaY
    }
    let loc = t.location(in:self.superview)
    let oldP = t.previousLocation(in:self.superview)
    let deltaX = loc.x - oldP.x
    let deltaY = loc.y - oldP.y
    var c = self.center
    if self.horiz {
        c.x += deltaX
    } else {
        c.y += deltaY
    }
    self.center = c
}

Look at how things are trending. We are maintaining multiple state properties, which we are managing across multiple methods, and we are subdividing a touch method implementation into tests depending on the state of our state machine. Our state machine is very simple, but already our code is becoming difficult to read and to maintain — and things will only become more messy as we try to make our view’s behavior more sophisticated.

Another area in which manual touch handling can rapidly prove overwhelming is when it comes to distinguishing between different gestures that the user is to be permitted to perform on a view. Imagine, for example, a view that distinguishes between a finger tapping briefly and a finger remaining down for a longer time. We can’t know how long a tap is until it’s over, so we must wait until then before deciding; once again, this requires maintaining state in a property (self.time):

override func touchesBegan(_ touches: Set<UITouch>, with e: UIEvent?) {
    self.time = touches.first!.timestamp
}
override func touchesEnded(_ touches: Set<UITouch>, with e: UIEvent?) {
    let diff = e!.timestamp - self.time
    if (diff < 0.4) {
        print("short")
    } else {
        print("long")
    }
}

A similar challenge is distinguishing between a single tap and a double tap. The UITouch tapCount property already makes this distinction, but that, by itself, is not enough to help us react differently to the two. What we must do, having received a tap whose tapCount is 1, is to use delayed performance in responding to it, so that we wait long enough to give a second tap a chance to arrive. This is unfortunate, because it means that if the user intends a single tap, some time will elapse before anything happens in response to it; however, there’s nothing we can readily do about that.

Distributing our various tasks correctly is tricky. We know when we have a double tap as early as touchesBegan(_:with:), but we respond to the double tap in touchesEnded(_:with:). Therefore we use a property (self.single) to communicate between the two. We don’t start our delayed response to a single tap until touchesEnded(_:with:), because what matters is the time between the taps as a whole, not between the starts of the taps:

override func touchesBegan(_ touches: Set<UITouch>, with e: UIEvent?) {
    let ct = touches.first!.tapCount
    switch ct {
    case 2:
        self.single = false
    default: break
    }
}
override func touchesEnded(_ touches: Set<UITouch>, with e: UIEvent?) {
    let ct = touches.first!.tapCount
    switch ct {
    case 1:
        self.single = true
        delay(0.3) {
            if self.single { // no second tap intervened
                print("single tap")
            }
        }
    case 2:
        print("double tap")
    default: break
    }
}

As that code weren’t confusing enough, let’s now consider combining our detection for a single or double tap with our earlier code for dragging a view horizontally or vertically. This is to be a view that can detect four kinds of gesture: a single tap, a double tap, a horizontal drag, and a vertical drag. We must include the code for all possibilities and make sure they don’t interfere with each other. The result is horrifying — a forced join between two already complicated sets of code, along with an additional pair of state properties (self.drag, self.decidedTapOrDrag) to track the decision between the tap gestures on the one hand and the drag gestures on the other:

override func touchesBegan(_ touches: Set<UITouch>, with e: UIEvent?) {
    // be undecided
    self.decidedTapOrDrag = false
    // prepare for a tap
    let ct = touches.first!.tapCount
    switch ct {
    case 2:
        self.single = false
        self.decidedTapOrDrag = true
        self.drag = false
        return
    default: break
    }
    // prepare for a drag
    self.decidedDirection = false
}
override func touchesMoved(_ touches: Set<UITouch>, with e: UIEvent?) {
    if self.decidedTapOrDrag && !self.drag {return}
    self.superview!.bringSubview(toFront:self)
    let t = touches.first!
    self.decidedTapOrDrag = true
    self.drag = true
    if !self.decidedDirection {
        self.decidedDirection = true
        let then = t.previousLocation(in:self)
        let now = t.location(in:self)
        let deltaX = abs(then.x - now.x)
        let deltaY = abs(then.y - now.y)
        self.horiz = deltaX >= deltaY
    }
    let loc = t.location(in:self.superview)
    let oldP = t.previousLocation(in:self.superview)
    let deltaX = loc.x - oldP.x
    let deltaY = loc.y - oldP.y
    var c = self.center
    if self.horiz {
        c.x += deltaX
    } else {
        c.y += deltaY
    }
    self.center = c
}
override func touchesEnded(_ touches: Set<UITouch>, with e: UIEvent?) {
    if !self.decidedTapOrDrag || !self.drag {
        // end for a tap
        let ct = touches.first!.tapCount
        switch ct {
        case 1:
            self.single = true
            delay(0.3) {
                if self.single {
                    print("single tap")
                }
            }
        case 2:
            print("double tap")
        default: break
        }
    }
}

That code seems to work, but it’s hard to say whether it covers all possibilities coherently; it’s barely legible and the logic borders on the mysterious. This is the kind of situation for which gesture recognizers were devised.

Gesture Recognizers

Writing and maintaining a state machine that interprets touches across a combination of three or four touch methods is hard enough when a view confines itself to expecting only one kind of gesture, such as dragging. It becomes even more involved when a view wants to accept and respond differently to different kinds of gesture. Furthermore, many types of gesture are conventional and standard; it seems insane to require developers to implement independently the elements that constitute what is, in effect, a universal vocabulary.

The solution is gesture recognizers, which standardize common gestures and allow the code for different gestures to be separated and encapsulated into different objects. Thanks to gesture recognizers, it is unnecessary to subclass UIView merely in order to implement touch analysis.

Gesture Recognizer Classes

A gesture recognizer (a subclass of UIGestureRecognizer) is an object whose job is to detect that a multitouch sequence equates to one particular type of gesture. It is attached to a UIView, which has for this purpose methods addGestureRecognizer(_:) and removeGestureRecognizer(_:), and a gestureRecognizers property.

A UIGestureRecognizer implements the four touch methods, but it is not a responder (a UIResponder), so it does not participate in the responder chain. If, however, a new touch is going to be delivered to a view, it is also associated with and delivered to that view’s gesture recognizers if it has any, and to that view’s superview’s gesture recognizers if it has any, and so on up the view hierarchy. Thus, the place of a gesture recognizer in the view hierarchy matters, even though it isn’t part of the responder chain.

UITouch and UIEvent provide complementary ways of learning how touches and gesture recognizers are associated. UITouch’s gestureRecognizers lists the gesture recognizers that are currently handling this touch. UIEvent’s touches(for:) can take a gesture recognizer parameter; it then lists the touches that are currently being handled by that gesture recognizer.

Each gesture recognizer maintains its own state as touch events arrive, building up evidence as to what kind of gesture this is. When one of them decides that it has recognized its own particular type of gesture, it emits either a single message (to indicate, for example, that a finger has tapped) or a series of messages (to indicate, for example, that a finger is moving); the distinction here is between a discrete and a continuous gesture.

What message a gesture recognizer emits, and to what object it sends it, is set through a target–action dispatch table attached to the gesture recognizer; a gesture recognizer is rather like a UIControl in this regard. Indeed, one might say that a gesture recognizer simplifies the touch handling of any view to be like that of a control. The difference is that one control may report several different control events, whereas each gesture recognizer reports only one gesture type, with different gestures being reported by different gesture recognizers.

UIGestureRecognizer itself is abstract, providing methods and properties to its subclasses. Among these are:

init(target:action:)

The designated initializer. Each message emitted by a UIGestureRecognizer is a matter of sending the action message to the target. Further target–action pairs may be added with addTarget(_:action:) and removed with removeTarget(_:action:).

Two forms of action: selector are possible: either there is no parameter, or there is a single parameter which will be the gesture recognizer. Most commonly, you’ll use the second form, so that the target can identify and query the gesture recognizer through one of its various methods and properties.

location(ofTouch:in:)

The second parameter is the view whose coordinate system you want to use. The touch is specified by an index number. The numberOfTouches property provides a count of current touches; the touches themselves are inaccessible by way of the gesture recognizer.

isEnabled

A convenient way to turn a gesture recognizer off without having to remove it from its view.

state, view

I’ll discuss state later on. The view is the view to which this gesture recognizer is attached.

Built-in UIGestureRecognizer subclasses are provided for six common gesture types: tap, pinch (inward or outward), pan (drag), swipe, rotate, and long press. Each embodies properties and methods likely to be needed for each type of gesture, either in order to configure the gesture recognizer beforehand or in order to query it as to the state of an ongoing gesture:

UITapGestureRecognizer (discrete)

Configuration: numberOfTapsRequired, numberOfTouchesRequired (“touches” means simultaneous fingers).

UIPinchGestureRecognizer (continuous)

Two fingers moving toward or away from each other. State: scale, velocity.

UIRotationGestureRecognizer (continuous)

Two fingers moving round a common center. State: rotation, velocity.

UISwipeGestureRecognizer (discrete)

A straight-line movement in one of the four cardinal directions. Configuration: direction (meaning permitted directions, a bitmask), numberOfTouchesRequired.

UIPanGestureRecognizer (continuous)

Dragging. Configuration: minimumNumberOfTouches, maximumNumberOfTouches. State: translation(in:), setTranslation(_:in:), velocity(in:); the coordinate system of the specified view is used.

UIScreenEdgePanGestureRecognizer

A UIPanGestureRecognizer subclass. It recognizes a pan gesture that starts at an edge of the screen. It adds a configuration property, edges, a UIRectEdge; despite the name (and the documentation), this must be set to a single edge.

UILongPressGestureRecognizer (continuous)

Configuration: numberOfTapsRequired, numberOfTouchesRequired, minimumPressDuration, allowableMovement. The numberOfTapsRequired is the count of taps before the tap that stays down, so it can be 0 (the default). The allowableMovement setting lets you compensate for the fact that the user’s finger is unlikely to remain steady during an extended press; thus we need to provide some limit before deciding that this gesture is, say, a drag, and not a long press after all. On the other hand, once the long press is recognized, the finger is permitted to drag as part of the long press gesture.

UIGestureRecognizer also provides a location(in:) method. This is a single point, even if there are multiple touches. The subclasses implement this variously. For example, for UIPanGestureRecognizer, the location is where the touch is if there’s a single touch, but it’s a sort of midpoint (“centroid”) if there are multiple touches.

We already know enough to implement, using a gesture recognizer, a view that responds to a single tap, or a view that responds to a double tap. Here’s code (probably from our view controller’s viewDidLoad) that implements a view (self.v) that responds to a single tap by calling our singleTap method:

let t1 = UITapGestureRecognizer(target:self, action:#selector(singleTap))
self.v.addGestureRecognizer(t1)

And here’s code that implements a view (self.v) that responds to a double tap by calling our doubleTap method:

let t2 = UITapGestureRecognizer(target:self, action:#selector(doubleTap))
t2.numberOfTapsRequired = 2
self.v.addGestureRecognizer(t2)

For a continuous gesture like dragging, we need to know both when the gesture is in progress and when the gesture ends. This brings us to the subject of a gesture recognizer’s state.

A gesture recognizer implements a notion of states (the state property, UIGestureRecognizerState); it passes through these states in a definite progression. The gesture recognizer remains in the .possible state until it can make a decision one way or the other as to whether this is in fact the correct gesture. The documentation neatly lays out the possible progressions:

Wrong gesture

.possible.failed. No action message is sent.

Discrete gesture (like a tap), recognized

.possible.ended. One action message is sent, when the state changes to .ended.

Continuous gesture (like a drag), recognized

.possible.began.changed (repeatedly) → .ended. Action messages are sent once for .began, as many times as necessary for .changed, and once for .ended.

Continuous gesture, recognized but later cancelled

.possible.began.changed (repeatedly) → .cancelled. Action messages are sent once for .began, as many times as necessary for .changed, and once for .cancelled.

The same action message arrives at the same target every time, so the action method must differentiate by asking about the gesture recognizer’s state. The usual implementation involves a switch statement.

To illustrate, we will implement, using a gesture recognizer, a view that lets itself be dragged around in any direction by a single finger. Our maintenance of state is greatly simplified, because a UIPanGestureRecognizer maintains a delta (translation) for us. This delta, available using translation(in:), is reckoned from the touch’s initial position. We don’t even need to record the view’s original center, because we can reset the UIPanGestureRecognizer’s delta, using setTranslation(_:in:):

func viewDidLoad {
    super.viewDidLoad()
    let p = UIPanGestureRecognizer(target:self, action:#selector(dragging))
    self.v.addGestureRecognizer(p)
}
func dragging(_ p : UIPanGestureRecognizer) {
    let v = p.view!
    switch p.state {
    case .began, .changed:
        let delta = p.translation(in:v.superview)
        var c = v.center
        c.x += delta.x; c.y += delta.y
        v.center = c
        p.setTranslation(.zero, in: v.superview)
    default: break
    }
}

To illustrate the use of a UIPanGestureRecognizer’s velocity(in:), let’s imagine a view that the user can drag, but which then springs back to where it was. We can express “springs back” with a spring animation (Chapter 4). All we have to do is add an .ended case to our dragging method (dest is the original center of our view v):

case .ended:
    let anim = UIViewPropertyAnimator(
        duration: 0.4,
        timingParameters: UISpringTimingParameters(
            dampingRatio: 0.6,
            initialVelocity: .zero))
    anim.addAnimations {
        v.center = dest
    }
    anim.startAnimation()

That’s good, but it would be even better if the view had some momentum at the moment the user lets go of it. If the user drags the view quickly away from its home and lets go, the view should keep moving a little in the same direction before springing back into place. That’s what the spring animation’s initialVelocity: parameter is for! We can easily find out what the view’s velocity is, at the moment the user releases it, by asking the gesture recognizer:

let vel = p.velocity(in: v.superview!)

Unfortunately, we cannot use this value directly as the spring animation’s initialVelocity; there’s a type impedance mismatch. The view’s velocity is expressed as a CGPoint measured in points per second. But the spring’s initialVelocity is expressed as a CGVector measured as a proportion of the distance to be travelled over the course of the animation. Fortunately, the conversion is easy:

case .ended:
    let vel = p.velocity(in: v.superview!)
    let c = v.center
    let distx = abs(c.x - dest.x)
    let disty = abs(c.y - dest.y)
    let anim = UIViewPropertyAnimator(
        duration: 0.4,
        timingParameters: UISpringTimingParameters(
            dampingRatio: 0.6,
            initialVelocity: CGVector(vel.x/distx, vel.y/disty)))
    anim.addAnimations {
        v.center = dest
    }
    anim.startAnimation()

A pan gesture recognizer can be used also to make a view draggable under the influence of a UIDynamicAnimator (Chapter 4). The strategy here is that the view is attached to one or more anchor points through a UIAttachmentBehavior; as the user drags, we move the anchor point(s), and the view follows. In this example, I set up the whole UIKit dynamics “stack” of objects as the gesture begins, anchoring the view at the point where the touch is; then I move the anchor point to stay with the touch. Instance properties self.anim and self.att store the UIDynamicAnimator and the UIAttachmentBehavior, respectively; self.view is our view’s superview, and is the animator’s reference view:

@IBAction func dragging(_ p: UIPanGestureRecognizer) {
    switch p.state {
    case .began:
        self.anim = UIDynamicAnimator(referenceView:self.view)
        let loc = p.location(ofTouch:0, in:p.view)
        let cen = CGPoint(p.view!.bounds.midX, p.view!.bounds.midY)
        let off = UIOffsetMake(loc.x-cen.x, loc.y-cen.y)
        let anchor = p.location(ofTouch:0, in:self.view)
        let att = UIAttachmentBehavior(item:p.view!,
            offsetFromCenter:off, attachedToAnchor:anchor)
        self.anim.addBehavior(att)
        self.att = att
    case .changed:
        self.att.anchorPoint = p.location(ofTouch:0, in: self.view)
    default:
        self.anim = nil
    }
}

The outcome is that the view both moves and rotates in response to dragging, like a plate being pulled about on a table by a single finger.

By adding behaviors to the dynamic animator, we can limit further what the view is permitted to do as it is being dragged by its anchor. For example, imagine a view that can be lifted vertically and dropped, but cannot be moved horizontally. As I demonstrated earlier, you can prevent horizontal dragging through the implementation of your response to touch events (and later in this chapter, I’ll show how to do this by subclassing UIPanGestureRecognizer). But the same sort of limitation can imposed by way of the underlying physics of the world in which the view exists — with a sliding attachment, for example.

Gesture Recognizer Conflicts

The question naturally arises of what happens when multiple gesture recognizers are in play. This isn’t a matter merely of multiple recognizers attached to a single view; as I have said, if a view is touched, not only its own gesture recognizers but also any gesture recognizers attached to views further up the view hierarchy are in play simultaneously. I like to think of a view as surrounded by a swarm of gesture recognizers — its own, and those of its superview, and so on. (In reality, it is a touch that has a swarm of gesture recognizers; that’s why a UITouch has a gestureRecognizers property, in the plural.)

The superview gesture recognizer swarm comes as a surprise to beginners, but it makes sense, because without it, certain gestures would be impossible. Imagine, for example, a pair of views, each of which the user can tap individually, but which the user can also touch simultaneously (one finger on each view) to rotate them together around their mutual centroid. Neither view can detect the rotation qua rotation, because neither view receives both touches; only the superview can detect it, so the fact that the views themselves respond to touches must not prevent the superview’s gesture recognizer from operating.

In general, once a gesture recognizer succeeds in recognizing its gesture, any other gesture recognizers associated with its touches are forced into the .failed state, and whatever touches were associated with those gesture recognizers are no longer sent to them; in effect, the first gesture recognizer in a swarm that recognizes its gesture owns the gesture (and its touches) from then on.

In many cases, this “first past the post” behavior, on its own, will correctly eliminate conflicts. If it doesn’t, you can modify it.

For example, we can add both our UITapGestureRecognizer for a single tap and our UIPanGestureRecognizer to a view and everything will just work; dragging works, and single tap works. Thus, “first past the post” is exactly the desired behavior.

What happens, though, if we also add the UITapGestureRecognizer for a double tap? Dragging works, and single tap works; double tap works too, but without preventing the single tap from working. So, on a double tap, both the single tap action method and the double tap action method are called. If that isn’t what we want, we don’t have to use delayed performance, as we did earlier. Instead, we can create a dependency between one gesture recognizer and another, telling the first to suspend judgement until the second has decided whether this is its gesture. We can do this by sending the first gesture recognizer the require(toFail:) message. (This method is rather badly named; it doesn’t mean “force this other recognizer to fail,” but rather, “you can’t succeed unless this other recognizer has failed.”)

So our view self.v is now configured as follows:

let t2 = UITapGestureRecognizer(target:self, action:#selector(doubleTap))
t2.numberOfTapsRequired = 2
self.v.addGestureRecognizer(t2)
let t1 = UITapGestureRecognizer(target:self, action:#selector(singleTap))
t1.require(toFail:t2) // *
self.v.addGestureRecognizer(t1)
let p = UIPanGestureRecognizer(target: self, action: #selector(dragging))
self.v.addGestureRecognizer(p)

Another conflict that can arise is between a gesture recognizer and a view that already knows how to respond to the same gesture, such as a UIControl. This problem pops up particularly when the gesture recognizer belongs to the UIControl’s superview. The UIControl’s mere presence does not “block” the superview’s gesture recognizer from recognizing a gesture on the UIControl, even if it is a UIControl that responds autonomously to touches. For example, your window’s root view might have a UITapGestureRecognizer attached to it (perhaps because you want to be able to recognize taps on the background), but there is also a UIButton within it. How is that gesture recognizer to ignore a tap on the button?

The UIView instance method gestureRecognizerShouldBegin(_:) solves the problem. It is called automatically; to modify its behavior, use a custom UIView subclass and override it. Its parameter is a gesture recognizer belonging to this view or to a view further up the view hierarchy. That gesture recognizer has recognized its gesture as taking place in this view; but by returning false, the view can tell the gesture recognizer to bow out and do nothing, not sending any action messages, and permitting this view to respond to the touch as if the gesture recognizer weren’t there.

Thus, for example, a UIButton could return false for a single tap UITapGestureRecognizer; a single tap on the button would then trigger the button’s action message and not the gesture recognizer’s action message. And in fact a UIButton, by default, does return false for a single tap UITapGestureRecognizer whose view is not the UIButton itself. (If the gesture recognizer is for some gesture other than a tap, then the problem never arises, because a tap on the button won’t cause the gesture recognizer to recognize in the first place.) Other built-in controls may also implement gestureRecognizerShouldBegin(_:) in such a way as to prevent accidental interaction with a gesture recognizer; the documentation says that a UISlider implements it in such a way that a UISwipeGestureRecognizer won’t prevent the user from sliding the “thumb,” and there may be other cases that aren’t documented explicitly. Naturally, you can take advantage of this feature in your own UIView subclasses as well.

Another way of resolving possible gesture recognizer conflicts is through the gesture recognizer’s delegate, or with a gesture recognizer subclass. I’ll discuss those in a moment.

Subclassing Gesture Recognizers

To subclass UIGestureRecognizer or a built-in gesture recognizer subclass, you must do the following things:

  • Import UIKit.UIGestureRecognizerSubclass. This allows you to set a gesture recognizer’s state (which is otherwise read-only), and exposes declarations for the methods you may need to override.

  • Override any touch methods you need to (as if the gesture recognizer were a UIResponder); if you’re subclassing a built-in gesture recognizer subclass, you will almost certainly call super so as to take advantage of the built-in behavior. In overriding a touch method, you need to think like a gesture recognizer. As these methods are called, a gesture recognizer is setting its state; you must participate in that process.

To illustrate, we will subclass UIPanGestureRecognizer so as to implement a view that can be moved only horizontally or vertically. Our strategy will be to make two UIPanGestureRecognizer subclasses — one that allows only horizontal movement, and another that allows only vertical movement. They will make their recognition decisions in a mutually exclusive manner, so we can attach an instance of each to our view. This encapsulates the decision-making logic in a gorgeously object-oriented way — a far cry from the spaghetti code we wrote earlier to do this same task.

I will show only the code for the horizontal drag gesture recognizer, because the vertical recognizer is symmetrically identical. We maintain just one property, self.origLoc, which we will use once to determine whether the user’s initial movement is horizontal. We override touchesBegan(_:with:) to set our property with the first touch’s location:

override func touchesBegan(_ touches: Set<UITouch>, with e: UIEvent) {
    self.origLoc = touches.first!.location(in:self.view!.superview)
    super.touchesBegan(touches, with:e)
}

We then override touchesMoved(_:with:); all the recognition logic is here. This method will be called for the first time with the state still at .possible. At that moment, we look to see if the user’s movement is more horizontal than vertical. If it isn’t, we set the state to .failed. But if it is, we just step back and let the superclass do its thing:

override func touchesMoved(_ touches: Set<UITouch>, with e: UIEvent) {
    if self.state == .possible {
        let loc = touches.first!.location(in:self.view!.superview)
        let deltaX = abs(loc.x - self.origLoc.x)
        let deltaY = abs(loc.y - self.origLoc.y)
        if deltaY >= deltaX {
            self.state = .failed
        }
    }
    super.touchesMoved(touches, with:e)
}

We now have a view that moves only if the user’s initial gesture is horizontal. But that isn’t the entirety of what we want; we want a view that, itself, moves horizontally only. To implement this, we’ll simply lie to our client about where the user’s finger is, by overriding translation(in:):

override func translation(in view: UIView?) -> CGPoint {
    var proposedTranslation = super.translation(in:view)
    proposedTranslation.y = 0
    return proposedTranslation
}

That example was simple, because we subclassed a fully functional built-in UIGestureRecognizer subclass. If you were to write your own UIGestureRecognizer subclass entirely from scratch, there would be more work to do:

  • You should definitely implement all four touch methods. Their job, at a minimum, is to advance the gesture recognizer through the canonical progression of its states. When the first touch arrives at a gesture recognizer, its state will be .possible; you never explicitly set the recognizer’s state to .possible yourself. As soon as you know this can’t be our gesture, you set the state to .failed (Apple says that a gesture recognizer should “fail early, fail often”). If the gesture gets past all the failure tests, you set the state instead either to .ended (for a discrete gesture) or to .began (for a continuous gesture); if .began, then you might set it to .changed, and ultimately you must set it to .ended. Action messages will be sent automatically at the appropriate moments.

  • You should probably implement reset. This is called after you reach the end of the progression of states to notify you that the gesture recognizer’s state is about to be set back to .possible; it is your chance to return your state machine to its starting configuration (resetting properties, for example).

Keep in mind that your gesture recognizer might stop receiving touches without notice. Just because it gets a touchesBegan(_:with:) call for a particular touch doesn’t mean it will ever get touchesEnded(_:with:) for that touch. If your gesture recognizer fails to recognize its gesture, either because it declares failure or because it is still in the .possible state when another gesture recognizer recognizes, it won’t get any more touch method calls for any of the touches that were being sent to it. This is why reset is so important; it’s the one reliable signal that it’s time to clean up and get ready to receive the beginning of another possible gesture.

Gesture Recognizer Delegate

A gesture recognizer can have a delegate (UIGestureRecognizerDelegate), which can perform two types of task.

These delegate methods can block a gesture recognizer’s operation:

gestureRecognizerShouldBegin(_:)

Sent to the delegate before the gesture recognizer passes out of the .possible state; return false to force the gesture recognizer to proceed to the .failed state. (This happens after gestureRecognizerShouldBegin(_:) has been sent to the view in which the touch took place. That view must not have returned false, or we wouldn’t have reached this stage.)

gestureRecognizer(_:shouldReceive:)

Sent to the delegate before a touch is sent to the gesture recognizer’s touchesBegan(_:with:) method; return false to prevent that touch from ever being sent to the gesture recognizer.

These delegate methods can mediate gesture recognition conflict:

gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:)

Sent when a gesture recognizer recognizes its gesture, if this will force the failure of another gesture recognizer, to the delegates of both gesture recognizers. Return true to prevent that failure, thus allowing both gesture recognizers to operate simultaneously. For example, a view could respond to both a two-fingered pinch and a two-fingered pan, the one applying a scale transform, the other changing the view’s center.

gestureRecognizer(_:shouldRequireFailureOf:)
gestureRecognizer(_:shouldBeRequiredToFailBy:)

Sent very early in the life of a gesture, when all gesture recognizers in a view’s swarm are still in the .possible state, to the delegates of all of them, pairing the gesture recognizer whose delegate this is with each of the other gesture recognizers in the swarm. Return true to prioritize between this pair of gesture recognizers, saying that one cannot succeed until the other has first failed. In essence, these delegate methods turn the decision made once and permanently in require(toFail:) into a live decision that can be made freshly every time a gesture occurs.

As an example, we will use delegate messages to combine a UILongPressGestureRecognizer and a UIPanGestureRecognizer, as follows: the user must perform a tap-and-a-half (tap, then tap and hold) to “get the view’s attention,” which we will indicate by a pulsing animation on the view; then (and only then) the user can drag the view.

The UIPanGestureRecognizer’s action method will take care of the drag, as shown earlier in this chapter. The UILongPressGestureRecognizer’s action method will take care of starting and stopping the animation:

func longPress(_ lp:UILongPressGestureRecognizer) {
    switch lp.state {
    case .began:
        let anim = CABasicAnimation(keyPath: #keyPath(CALayer.transform))
        anim.toValue = CATransform3DMakeScale(1.1, 1.1, 1)
        anim.fromValue = CATransform3DIdentity
        anim.repeatCount = .infinity
        anim.autoreverses = true
        lp.view!.layer.add(anim, forKey:nil)
    case .ended, .cancelled:
        lp.view!.layer.removeAllAnimations()
    default: break
    }
}

As we created our gesture recognizers, we kept a reference to the UILongPressGestureRecognizer (self.longPresser), and we made ourself the UIPanGestureRecognizer’s delegate. So we will receive delegate messages. If the UIPanGestureRecognizer tries to declare success while the UILongPressGestureRecognizer’s state is .failed or still at .possible, we prevent it. If the UILongPressGestureRecognizer succeeds, we permit the UIPanGestureRecognizer to operate as well:

func gestureRecognizerShouldBegin(_ g: UIGestureRecognizer) -> Bool {
    switch self.longPresser.state {
    case .possible, .failed:
        return false
    default:
        return true
    }
}
func gestureRecognizer(_ g: UIGestureRecognizer,
    shouldRecognizeSimultaneouslyWith g2: UIGestureRecognizer) -> Bool {
        return true
}

The result is that the view can be dragged only while it is pulsing; in effect, what we’ve done is to compensate, using delegate methods, for the fact that UIGestureRecognizer has no require(toSucceed:) method.

If you are subclassing a gesture recognizer class, you can incorporate delegate-like behavior into the subclass, by overriding the following methods:

  • canPrevent(_:)

  • canBePrevented(by:)

  • shouldRequireFailure(of:)

  • shouldBeRequiredToFail(by:)

The prevent methods are similar to the delegate shouldBegin method, and the fail methods are similar to the delegate fail methods. In this way, you can mediate gesture recognizer conflict at the class level. The built-in gesture recognizer subclasses already do this; that is why, for example, a single tap UITapGestureRecognizer does not, by recognizing its gesture, cause the failure of a double tap UITapGestureRecognizer.

You can also, in a gesture recognizer subclass, send ignore(_:for:) directly to a gesture recognizer (typically, to self) to ignore a specific touch of a specific event. This has the same effect as the delegate method gestureRecognizer(_:shouldReceive:) returning false, blocking all future delivery of that touch to the gesture recognizer. For example, if you’re in the middle of an already recognized gesture and a new touch arrives, you might elect to ignore it.

Gesture Recognizers in the Nib

Instead of instantiating a gesture recognizer in code, you can create and configure it in a .xib or .storyboard file. In the nib editor, drag a gesture recognizer from the Object library onto a view; the gesture recognizer becomes a top-level nib object, and the view’s gestureRecognizers outlet is connected to the gesture recognizer. (You can add more than one gesture recognizer to a view in the nib: the view’s gestureRecognizers property is an array, and its gestureRecognizers outlet is an outlet collection.) The gesture recognizer’s properties are configurable in the Attributes inspector, and the gesture recognizer has a delegate outlet. The gesture recognizer is a full-fledged nib object, so you can make an outlet to it.

To configure a gesture recognizer’s target–action pair in the nib editor, treat it like a UIControl’s control event. The action method’s signature should be marked @IBAction, and it should take a single parameter, which will be a reference to the gesture recognizer. You will then be able to form the Sent Action connection in the usual way. (A gesture recognizer can have multiple target–action pairs, but only one target–action pair can be configured for a gesture recognizer using the nib editor.) A view retains its gesture recognizers, so there will usually be no need for additional memory management on a gesture recognizer instantiated from a nib.

3D Touch Press Gesture

On a device with 3D touch, you can treat pressing as kind of gesture. It isn’t formally a gesture; there is, unfortunately, no 3D touch press gesture recognizer. Nevertheless, your code can easily detect a 3D touch press and respond dynamically to the degree of force being applied.

The simplest way to implement such a response is through the UIPreviewInteraction class (new in iOS 10). You initialize a UIPreviewInteraction object with the view in which pressing is to be detected, retain the UIPreviewInteraction object, and assign it a delegate (adopting the UIPreviewInteractionDelegate protocol). The delegate is sent these messages, starting when the user begins to apply force within the view:

previewInteractionShouldBegin(_:)

Optional. Return false to ignore this press gesture. Among other things, this method might query the UIPreviewInteraction’s view and location(in:) to decide how to proceed.

previewInteraction(_:didUpdatePreviewTransition:ended:)

The amount of applied force has changed. The amount of force is reported (in the second parameter) as a value between 0 and 1. When 1 is reached, ended: is also true, and the device vibrates.

previewInteraction(_:didUpdateCommitTransition:ended:)

Optional; behaves exactly like the previous method. If implemented, the gesture has two stages, increasing from 0 to 1 and reported by didUpdatePreview, and then increasing from 0 to 1 and reported by didUpdateCommit.

previewInteractionDidCancel(_:)

The user has backed off the gesture completely, before reaching ended: (or the touch was cancelled for some other reason, such a phone call arriving).

To illustrate, imagine a sort of Whack-a-Mole game where the user is to remove views by pressing each one. (In real life, of course, there would also need to be a way to play the game on a device that lacks 3D touch.) As the user presses, we’ll apply a scale transform to the view, increasing its apparent size in proportion to the amount of force, while at the same time fading the view away by decreasing its opacity; if the user reaches a full press, we’ll remove the view completely.

We’ll implement this in the simplest possible way. The code will all go into the pressable view itself. When the view is added to its superview, it creates and configures the UIPreviewInteraction object, storing it in an instance property (self.prev):

override func didMoveToSuperview() {
    self.prev = UIPreviewInteraction(view: self)
    self.prev.delegate = self
}

As force reports arrive, we’ll increase the view’s scale transform and decrease its opacity accordingly:

func previewInteraction(_ prev: UIPreviewInteraction,
    didUpdatePreviewTransition prog: CGFloat,
    ended: Bool) {
        let scale = prog + 1
        self.transform = CGAffineTransform(scaleX: scale, y: scale)
        let alph = ((1-prog)*0.6) + 0.3
        self.alpha = alph
        if ended { // device vibrates
            self.removeFromSuperview()
        }
}

If the user backs off the gesture completely, we’ll remove the transform and restore our opacity:

func previewInteractionDidCancel(_ prev: UIPreviewInteraction) {
    self.transform = .identity
    self.alpha = 1
}

That’s all! The view now expands and explodes off the screen with a satisfying pop (“haptic feedback”) as the user presses on it.

Instead of applying the transform ourselves, directly, we might use a property animator, taking advantage of the fact that it represents a “frozen” animation (“Frozen View Animation”). Here’s a rewrite in which a property animator is used (held in an instance property, self.anim):

func makeAnimator() {
    self.anim = UIViewPropertyAnimator(duration: 1, curve: .linear) {
        [unowned self] in
        self.alpha = 0.3
        self.transform = CGAffineTransform(scaleX: 2, y: 2)
    }
}
override func didMoveToSuperview() {
    self.prev = UIPreviewInteraction(view: self)
    self.prev.delegate = self
    self.makeAnimator()
}
func previewInteractionDidCancel(_ prev: UIPreviewInteraction) {
    self.anim.pauseAnimation()
    self.anim.isReversed = true
    self.anim.addCompletion { _ in self.makeAnimator() }
    self.anim.continueAnimation(
        withTimingParameters: nil, durationFactor: 0.01)
}
func previewInteraction(_ prev: UIPreviewInteraction,
    didUpdatePreviewTransition prog: CGFloat,
    ended: Bool) {
        var prog = prog // clamp fraction complete
        if prog < 0.05 {prog = 0.05}
        if prog > 0.95 {prog = 0.95}
        self.anim.fractionComplete = prog
        if ended {
            self.anim.stopAnimation(false)
            self.anim.finishAnimation(at: .end)
            self.removeFromSuperview()
        }
}

Touch Delivery

Here’s the full standard procedure by which a touch is delivered to views and gesture recognizers:

  • Whenever a new touch appears, the application performs hit-testing to determine the view that was touched. This view will be permanently associated with this touch, and is called, appropriately, the hit-test view. The logic of ignoring a view (denying it the ability to become the hit-test view) in response to its isUserInteractionEnabled, isHidden, and alpha properties is implemented at this stage.

  • Each time the touch situation changes, the application calls its own sendEvent(_:), which in turn calls the window’s sendEvent(_:). The window delivers each of an event’s touches by calling the appropriate touch method(s), as follows:

    • As a touch first appears, the logic of obedience to isMultipleTouchEnabled and isExclusiveTouch is considered. If permitted by that logic:

      • The touch is delivered to the hit-test view’s swarm of gesture recognizers.

      • The touch is delivered to the hit-test view itself.

    • If a gesture is recognized by a gesture recognizer, then for any touch associated with this gesture recognizer:

      • touchesCancelled(_:for:) is sent to the touch’s view, and the touch is no longer delivered to its view.

      • If the touch was associated with any other gesture recognizer, that gesture recognizer is forced to fail.

    • If a gesture recognizer fails, either because it declares failure or because it is forced to fail, its touches are no longer delivered to it, but (except as already specified) they continue to be delivered to their view.

    • If a touch would be delivered to a view, but that view does not respond to the appropriate touch method, a responder further up the responder chain is sought that does respond to it, and the touch is delivered there.

The rest of this chapter discusses the details of touch delivery. As you’ll see, nearly every bit of the standard procedure can be customized to some extent.

Hit-Testing

Hit-testing is the determination of what view the user touched. View hit-testing uses the UIView instance method hitTest(_:with:), whose first parameter is the CGPoint of interest. It returns either a view (the hit-test view) or nil. The idea is to find the frontmost view containing the touch point. This method uses an elegant recursive algorithm, as follows:

  1. A view’s hitTest(_:with:) first calls the same method on its own subviews, if it has any, because a subview is considered to be in front of its superview. The subviews are queried in front-to-back order (Chapter 1): thus, if two sibling views overlap, the one in front reports the hit first.

  2. If, as a view hit-tests its subviews, any of those subviews responds by returning a view, it stops querying its subviews and immediately returns the view that was returned to it. Thus, the very first view to declare itself the hit-test view immediately percolates all the way to the top of the call chain and is the hit-test view.

  3. If, on the other hand, a view has no subviews, or if all of its subviews return nil (indicating that neither they nor their subviews was hit), then the view calls point(inside:with:) on itself. If this call reveals that the touch was inside this view, the view returns itself, declaring itself the hit-test view; otherwise it returns nil.

(No problem arises if a view has a transform, because point(inside:with:) takes the transform into account. That’s why a rotated button continues to work correctly.)

It is also up to hitTest(_:with:) to implement the logic of touch restrictions exclusive to a view. If a view’s isUserInteractionEnabled is false, or its isHidden is true, or its alpha is close to 0.0, it returns nil without hit-testing any of its subviews and without calling point(inside:with:). Thus these restrictions do not, of themselves, exclude a view from being hit-tested; on the contrary, they operate precisely by modifying a view’s hit-test result.

However, hit-testing knows nothing about isMultipleTouchEnabled (which involves multiple touches) or isExclusiveTouch (which involves multiple views). The logic of obedience to these properties is implemented at a later stage of the story.

Performing Hit-Testing

You can perform hit-testing yourself at any moment where it might prove useful. In calling hitTest(_:with:), supply a point in the coordinates of the view to which the message is sent. The second parameter is supposed to be a UIEvent, but it can be nil if you have no event.

For example, suppose we have a superview with two UIImageView subviews. We want to detect a tap in either UIImageView, but we want to handle this at the level of the superview. We can attach a UITapGestureRecognizer to the superview, but then the gesture recognizer’s view is the superview, so how will we know which subview, if any, the tap was in?

First, ensure that isUserInteractionEnabled is true for both UIImageViews. UIImageView is one of the few built-in view classes where this property is false by default, and a view whose isUserInteractionEnabled is false won’t normally be the result of a call to hitTest(_:with:). Then, when our gesture recognizer’s action method is called, we can perform hit-testing to determine where the tap was:

// g is the gesture recognizer
let p = g.location(ofTouch:0, in: g.view)
let v = g.view?.hitTest(p, with: nil)
if let v = v as? UIImageView { // ...

Hit-Test Munging

You can override hitTest(_:with:) in a view subclass, to alter its results during touch delivery, thus customizing the touch delivery mechanism. I call this hit-test munging. Hit-test munging can be used selectively as a way of turning user interaction on or off in an area of the interface. In this way, some unusual effects can be produced.

An important use of hit-test munging is to permit the touching of parts of subviews outside the bounds of their superview. If a view’s clipsToBounds is false, a paradox arises: the user can see the regions of its subviews that are outside its bounds, but can’t touch them. This can be confusing and seems wrong. The solution is for the view to override hitTest(_:with:) as follows:

override func hitTest(_ point: CGPoint, with e: UIEvent?) -> UIView? {
    if let result = super.hitTest(point, with:e) {
        return result
    }
    for sub in self.subviews.reversed() {
        let pt = self.convert(point, to:sub)
        if let result = sub.hitTest(pt, with:e) {
            return result
        }
    }
    return nil
}

Hit-Testing For Layers

Layers do not receive touches. A touch is reported to a view, not a layer. A layer, except insofar as it is a view’s underlying layer and gets touch reporting because of its view, is completely untouchable; from the point of view of touches and touch reporting, it’s as if the layer weren’t on the screen at all. No matter where a layer may appear to be, a touch falls through the layer, to whatever view is behind it.

Nevertheless, hit-testing for layers is possible. It doesn’t happen automatically, as part of sendEvent(_:) or anything else; it’s up to you. It’s just a convenient way of finding out which layer would receive a touch at a point, if layers did receive touches. To hit-test layers, call hitTest(_:) on a layer, with a point in superlayer coordinates.

In the case of a layer that is a view’s underlying layer, you don’t need hit-testing. It is the view’s drawing; where it appears is where the view is. So a touch in that layer is equivalent to a touch in its view. Indeed, one might say (and it is often said) that this is what views are actually for: to provide layers with touchability.

The only layers on which you’d need special hit-testing, then, would presumably be layers that are not themselves any view’s underlying layer, because those are the only ones you don’t find out about by normal view hit-testing. However, all layers, including a layer that is its view’s underlying layer, are part of the layer hierarchy, and can participate in layer hit-testing. So the most comprehensive way to hit-test layers is to start with the topmost layer, the window’s layer. In this example, we subclass UIWindow (see Chapter 1) and override its hitTest(_:with:) so as to get layer hit-testing every time there is view hit-testing:

override func hitTest(_ point: CGPoint, with e: UIEvent?) -> UIView? {
    let lay = self.layer.hitTest(point)
    // ... possibly do something with that information
    return super.hitTest(point, with:e)
}

In that example, self is the window, which is a special case. In general, you’ll have to convert to superlayer coordinates. In this next example, we return to the CompassView developed in Chapter 3, in which all the parts of the compass are layers; we want to know whether the user tapped on the arrow layer, and if so, we’ll rotate the arrow. For simplicity, we’ve given the CompassView a UITapGestureRecognizer, and this is its action method, in the CompassView itself. We convert to our superview’s coordinates, because these are also our layer’s superlayer coordinates:

@IBAction func tapped(_ t:UITapGestureRecognizer) {
    let p = t.location(ofTouch:0, in: self.superview)
    let hitLayer = self.layer.hitTest(p)
    let arrow = (self.layer as! CompassLayer).arrow!
    if hitLayer == arrow { // respond to touch
        arrow.transform = CATransform3DRotate(
            arrow.transform, .pi/4.0, 0, 0, 1)
    }
}

Layer hit-testing knows nothing of the restrictions on touch delivery; it just reports on every sublayer, even one whose view (for example) has isUserInteractionEnabled set to false.

Hit-Testing For Drawings

The preceding example (letting the user tap on the compass arrow) does work, but we might complain that it is reporting a hit on the arrow layer even if the hit misses the drawing of the arrow. That’s true for view hit-testing as well. A hit is reported if we are within the view or layer as a whole; hit-testing knows nothing of drawing, transparent areas, and so forth.

If you know how the region is drawn and can reproduce the edge of that drawing as a CGPath, you can call contains(_:using:transform:) to test whether a point is inside it. So, in our compass layer, we could override hitTest(_:) along these lines:

override func hitTest(_ p: CGPoint) -> CALayer? {
    var lay = super.hitTest(p)
    if lay == self.arrow {
        let pt = self.arrow.convert(p, from:self.superlayer)
        let path = CGMutablePath()
        path.addRect(CGRect(10,20,20,80))
        path.move(to:CGPoint(0, 25))
        path.addLine(to:CGPoint(20, 0))
        path.addLine(to:CGPoint(40, 25))
        path.closeSubpath()
        if !path.contains(pt, using: .winding) {
            lay = nil
        }
    }
    return lay
}

Alternatively, it might be the case that if a pixel of the drawing is transparent, it’s outside the drawn region, so that it suffices to detect whether the pixel tapped is transparent. Unfortunately, there’s no built-in way to ask a drawing (or a view, or a layer) for the color of a pixel. Instead, you have to make a bitmap graphics context and copy the drawing into it, and then ask the bitmap for the color of a pixel. If you can reproduce the content as an image, and all you care about is transparency, you can make a one-pixel alpha-only bitmap, draw the image in such a way that the pixel you want to test is the pixel drawn into the bitmap, and examine the transparency of the resulting pixel. In this example, im is our UIImage and point is the coordinates of the pixel we want to test:

let info = CGImageAlphaInfo.alphaOnly.rawValue
let pixel = UnsafeMutablePointer<UInt8>.allocate(capacity:1)
defer {
    pixel.deinitialize(count: 1)
    pixel.deallocate(capacity:1)
}
pixel[0] = 0
let sp = CGColorSpaceCreateDeviceGray()
let context = CGContext(data: pixel,
    width: 1, height: 1, bitsPerComponent: 8, bytesPerRow: 1,
    space: sp, bitmapInfo: info)!
UIGraphicsPushContext(context)
im.draw(at:CGPoint(-point.x, -point.y))
UIGraphicsPopContext()
let p = pixel[0]
let alpha = Double(p)/255.0
let transparent = alpha < 0.01

However, there may not be a one-to-one relationship between the pixels of the underlying drawing and the points of the drawing as portrayed on the screen — because the drawing is stretched, for example. In many cases, the CALayer method render(in:) can be helpful here. This method allows you to copy a layer’s actual drawing into a graphics context of your choice. If that context is an image context (Chapter 2), you can use the resulting image as im in the preceding code.

Hit-Testing During Animation

Making a view user-touchable while it is being animated is a tricky problem. The reason is that the view may not be located where the user sees it. Recall (from Chapter 4) that the animation is just an “animation movie” — what the user sees is the presentation layer. The view itself, which the user is trying to touch, is at the location of the model layer.

Therefore, if user interaction is allowed during an animation that moves a view from one place to another, then if the user taps where the animated view appears to be, the tap might mysteriously fail because the actual view is elsewhere; conversely, the user might accidentally tap where the view actually is, and the tap will hit the animated view even though it appears to be elsewhere.

A simple solution is to disallow touchability entirely during an animation. You can temporarily turn off your entire interface’s touchability with UIApplication’s beginIgnoringInteractionEvents, as I mentioned earlier in this chapter. Even if you don’t do that, view animation ordered through a UIView class method, by default, turns off touchability of a view while it is being animated — though you can override that with .allowUserInteraction in the options: argument.

But what if you do want the user to be able to interact with an animated view? New in iOS 10, there’s a simple solution for that, too: use a property animator! A property animator makes touching an animated view easy; that is part of its purpose.

Before demonstrating that, let’s talk about what you have to do if you’re not using a property animator. The trick, in that case, to hit-test the presentation layer. In this simple example, we have a superview containing a subview. To allow the user to tap on the subview even when it is being animated, we implement hit-test munging in the subview:

override func hitTest(_ point: CGPoint, with e: UIEvent?) -> UIView? {
    let pres = self.layer.presentation()!
    let suppt = self.convert(point, to: self.superview!)
    let prespt = self.superview!.layer.convert(suppt, to: pres)
    return super.hitTest(prespt, with: e)
}

That works. At the same time, however, the animated view, as Apple puts it in the WWDC 2011 videos, “swallows the touch.” For example, suppose the view in motion is a button. Although our hit-test munging makes it possible for the user to tap the button as it is being animated, and although the user sees the button highlight in response, the button’s action message is not sent in response to this highlighting if the animation is in-flight when the tap takes place. This behavior seems unfortunate, but it’s generally possible to work around it — for instance, with a gesture recognizer.

How does a property animator help? By default, a property animator’s isUserInteractionEnabled is true. That means the animated view is touchable. As long as you don’t set the property animator’s isManualHitTestingEnabled to true, the property animator will hit-test the animated view’s presentation layer for you, so that you don’t have to. (If you do set isManualHitTestingEnabled to true, the job of hit-testing is turned back over to you; you might want to do this in complicated situations where the property animator’s hit-test munging isn’t sufficient.)

So view touchability, with a property animator, is the default. Moreover, the animated view doesn’t “swallow the touch.” To animate a button that remains tappable while the animation is in-flight, just animate the button:

let anim = UIViewPropertyAnimator(duration: 10, curve: .linear) {
    self.button.center = goal
}
anim.startAnimation()

So a property animator’s animated view is touchable by the user. And, as I explained in Chapter 4, a property animator’s animation is interruptible. Together, these two facts mean that, with a property animator, it is easy to alternate between a view’s being animated and the same view’s being manipulated by the user.

In this example, we extend the preceding example. The view is slowly animating its way toward the goal position. But at any time, the user can grab it and drag it around (during which time, the animation is interrupted). As soon as the user releases the view, the animation resumes: the view continues on its way toward the goal position.

Obviously, in order to be draggable, the view has a UIPanGestureRecognizer. All the work takes place in the gesture recognizer’s action method. As usual, we have a switch statement that tests the gesture recognizer’s state. In the .began case, we interrupt the animation so that dragging can happen:

case .began:
    if anim.state == .active {
        anim.stopAnimation(true)
    }
    fallthrough

The .changed case is our usual code for making a view draggable, so I won’t bother to repeat it here. The .ended case is the really interesting part.

Our aim here is to resume animating the view from wherever it now is toward the goal. We can do this by supplying the same animation and calling continueAnimation(withTimingParameters:durationFactor:). But we need first to calculate the durationFactor: argument. This needs to be a ratio proportional to our original duration. In my implementation, the speed at which the view moves should remain the same. Thus, the ratio of durations is the ratio of distances — the distance of the view’s original position from the goal, and its current distance from the goal:

case .ended:
    // how far are we from the goal relative to original distance?
    func pyth(_ pt1:CGPoint, _ pt2:CGPoint) -> CGFloat {
        let x = pt1.x - pt2.x
        let y = pt1.y - pt2.y
        return sqrt(x*x + y*y)
    }
    let origd = pyth(self.oldButtonCenter, self.goal)
    let curd = pyth(v.center, self.goal)
    let factor = curd/origd
    anim.addAnimations {
        self.button.center = self.goal
    }
    anim.startAnimation()
    anim.pauseAnimation()
    anim.continueAnimation(
        withTimingParameters: anim.timingParameters,
        durationFactor: factor)

Initial Touch Event Delivery

When the touch situation changes, an event containing all touches is handed to the UIApplication instance by calling its sendEvent(_:), and the UIApplication in turn hands it to the UIWindow by calling its sendEvent(_:). The UIWindow then performs the complicated logic of examining, for every touch, the hit-test view and its superviews and their gesture recognizers, and deciding which of them should be sent a touch method call.

You can override sendEvent(_:) in a subclass of UIWindow or UIApplication. These are delicate and crucial maneuvers, however, and you wouldn’t want to lame your application by interfering with them. Moreover, it is unlikely, nowadays, that you would need to resort to such measures. A typical case before the advent of gesture recognizers was that you needed to detect touches directed to an object of some built-in interface class in a way that subclassing it wouldn’t permit. For example, you want to know when the user swipes a UIWebView; you’re not allowed to subclass UIWebView, and in any case it eats the touch. The solution used to be to subclass UIWindow and override sendEvent(_:); you would then work out whether this was a swipe on the UIWebView and respond accordingly, or else call super. Now, however, you can attach a UISwipeGestureRecognizer to the UIWebView.

Gesture Recognizer and View

When a touch first appears and is delivered to a gesture recognizer, it is also delivered to its hit-test view, the same touch method being called on both. This is the most reasonable approach, as it means that touch interpretation by a view isn’t jettisoned just because gesture recognizers are in the picture. Later on in the multitouch sequence, if all the gesture recognizers in a view’s swarm declare failure to recognize their gesture, that view’s internal touch interpretation just proceeds as if gesture recognizers had never been invented.

Moreover, touches and gestures are two different things; sometimes you want to respond to both. In one of my apps, where the user can tap cards, each card has a single tap gesture recognizer and a double tap gesture recognizer, but it also responds directly to touchesBegan(_:with:) by reducing its own opacity, and to touchesEnded(_:with:) and touchesCancelled(_:with:) by restoring its opacity. The result is that the user always sees feedback when touching a card, instantly, regardless of what the gesture turns out to be.

Later, if a gesture recognizer in a view’s swarm recognizes its gesture, that view is sent touchesCancelled(_:with:) for any touches that went to that gesture recognizer and were hit-tested to that view, and subsequently the view no longer receives those touches. This behavior can be changed by setting a gesture recognizer’s cancelsTouchesInView property to false. If this is the case for every gesture recognizer in a view’s swarm, the view will receive touch events more or less as if no gesture recognizers were in the picture.

If a gesture recognizer happens to be ignoring a touch (because, for example, it was told to do so by ignore(_:for:)), then touchesCancelled(_:with:) won’t be sent to the view for that touch when that gesture recognizer recognizes its gesture. Thus, a gesture recognizer’s ignoring a touch is the same as simply letting it fall through to the view, as if the gesture recognizer weren’t there.

Gesture recognizers can also delay the delivery of touches to a view, and by default they do. The UIGestureRecognizer property delaysTouchesEnded is true by default, meaning that when a touch reaches .ended and the gesture recognizer’s touchesEnded(_:with:) is called, if the gesture recognizer is still allowing touches to be delivered to the view because its state is still .possible, it doesn’t deliver this touch until it has resolved the gesture. When it does, either it will recognize the gesture, in which case the view will have touchesCancelled(_:with:) called instead (as already explained), or it will declare failure and now the view will have touchesEnded(_:with:) called.

The reason for this behavior is most obvious with a gesture where multiple taps are required. The first tap ends, but this is insufficient for the gesture recognizer to declare success or failure, so it withholds that touch from the view. In this way, the gesture recognizer gets the proper priority. In particular, if there is a second tap, the gesture recognizer should succeed and send touchesCancelled(_:with:) to the view — but it can’t do that if the view has already been sent touchesEnded(_:with:).

It is also possible to delay the entire suite of touch methods from being called on a view, by setting a gesture recognizer’s delaysTouchesBegan property to true. Again, this delay would be until the gesture recognizer can resolve the gesture: either it will recognize it, in which case the view will have touchesCancelled(_:with:) called, or it will declare failure, in which case the view will receive touchesBegan(_:with:) plus any further touch method calls that were withheld — except that it will receive at most one touchesMoved(_:with:) call, the last one, because if a lot of these were withheld, to queue them all up and send them all at once now would be simply insane.

It is unlikely that you’ll change a gesture recognizer’s delaysTouchesBegan property to true, however. You might do so, for example, if you have an elaborate touch analysis within a view that simply cannot operate simultaneously with a gesture recognizer, but this is improbable, and the latency involved may look strange to your user.

When touches are delayed and then delivered, what’s delivered is the original touch with the original event, which still have their original timestamps. Because of the delay, these timestamps may differ significantly from now. For this reason (and many others), Apple warns that touch analysis that is concerned with timing should always look at the timestamp, not the clock.

Touch Exclusion Logic

It is up to the UIWindow’s sendEvent(_:) to implement the logic of isMultipleTouchEnabled and isExclusiveTouch.

If a new touch is hit-tested to a view whose isMultipleTouchEnabled is false and which already has an existing touch hit-tested to it, then sendEvent(_:) never delivers the new touch to that view. However, that touch is delivered to the view’s swarm of gesture recognizers.

Similarly, if there’s an isExclusiveTouch view in the window, then sendEvent(_:) must decide whether a particular touch should be delivered, in accordance with the meaning of isExclusiveTouch, which I described earlier. If a touch is not delivered to a view because of isExclusiveTouch restrictions, it is not delivered to its swarm of gesture recognizers either.

For example, suppose you have two views with touch handling, and their common superview has a pinch gesture recognizer. Normally, if you touch both views simultanously and pinch, the pinch gesture recognizer recognizes. But if both views are marked isExclusiveTouch, the pinch gesture recognizer does not recognize.

Gesture Recognition Logic

When a gesture recognizer recognizes its gesture, everything changes. As we’ve already seen, the touches for this gesture recognizer are sent to their hit-test views as a touchesCancelled(_:with:) message, and then no longer arrive at those views (unless the gesture recognizer’s cancelsTouchesInView is false). Moreover, all other gesture recognizers pending with regard to these touches are made to fail, and then are no longer sent the touches they were receiving either.

If the very same event would cause more than one gesture recognizer to recognize, there’s an algorithm for picking the one that will succeed and make the others fail: a gesture recognizer lower down the view hierarchy (closer to the hit-test view) prevails over one higher up the hierarchy, and a gesture recognizer more recently added to its view prevails over one less recently added.

There are various means for modifying this “first past the post” behavior:

Dependency order

Certain methods institute a dependency order, causing a gesture recognizer to be put on hold when it tries to transition from the .possible state to the .began (continuous) or .ended (discrete) state; only if a certain other gesture recognizer fails is this one permitted to perform that transition. Apple says that in a dependency like this, the gesture recognizer that fails first is not sent reset (and won’t receive any touches) until the second finishes its state sequence and is sent reset, so that they resume recognizing together. The methods are:

  • require(toFail:) sent to a gesture recognizer

  • shouldRequireFailure(of:) in a subclass

  • shouldBeRequiredToFail(by:) in a subclass

  • gestureRecognizer(_:shouldRequireFailureOf:) in the delegate

  • gestureRecognizer(_:shouldBeRequiredToFailBy:) in the delegate

The first of those methods sets up a permanent relationship between two gesture recognizers, and cannot be undone; but the others are sent every time a gesture starts in a view whose swarm includes both gesture recognizers, and each applies only on this occasion.

The delegate methods work together as follows. For each pair of gesture recognizers in the hit-test view’s swarm, the members of that pair are arranged in a fixed order (as I’ve already described). The first of the pair is sent shouldRequire and then shouldBeRequired, and then the second of the pair is sent shouldRequire and then shouldBeRequired. But if any of those four methods returns true, the relationship between that pair is settled and we proceed to the next pair.

Success into failure

Certain methods, by returning false, turn success into failure; at the moment when the gesture recognizer is about to declare that it recognizes its gesture, transitioning from the .possible state to the .began (continuous) or .ended (discrete) state, it is forced to fail instead:

  • UIView’s gestureRecognizerShouldBegin(_:)

  • The delegate’s gestureRecognizerShouldBegin(_:)

Simultaneous recognition

A gesture recognizer succeeds, but some other gesture recognizer is not forced to fail, in accordance with these methods:

  • gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:) in the delegate

  • canPrevent(_:) in a subclass

  • canBePrevented(by:) in a subclass

In the subclass methods, prevent means “by succeeding, you force failure upon this other,” and bePrevented means “by succeeding, this other forces failure on you.” They work together as follows. canPrevent is called first; if it returns false, that’s the end of the story for that gesture recognizer, and canPrevent is called on the other gesture recognizer. But if canPrevent returns true when it is first called, the other gesture recognizer is sent canBePrevented. If it returns true, that’s the end of the story; if it returns false, the process starts over the other way around, sending canPrevent to the second gesture recognizer, and so forth. In this way, conflicting answers are resolved without the device exploding: prevention is regarded as exceptional (even though it is in fact the norm) and will happen only if it is acquiesced to by everyone involved.

Touches and the Responder Chain

A UIView is a responder, and participates in the responder chain. In particular, if a touch is to be delivered to a UIView (because, for example, it’s the hit-test view) and that view doesn’t implement the relevant touch method, a walk up the responder chain is performed, looking for a responder that does implement it; if such a responder is found, the touch is delivered to that responder. Moreover, the default implementation of the touch methods — the behavior that you get if you call super — is to perform the same walk up the responder chain, starting with the next responder in the chain.

The relationship between touch delivery and the responder chain can be useful, but you must be careful not to allow it to develop into an incoherency. For example, if touchesBegan(_:with:) is implemented in a superview but not in a subview, then a touch to the subview will result in the superview’s touchesBegan(_:with:) being called, with the first parameter (the touches) containing a touch whose view is the subview. But most UIView implementations of the touch methods rely upon the assumption that the first parameter consists of all and only touches whose view is self; built-in UIView subclasses certainly assume this.

Here are some suggested guidelines for rational use of the responder chain:

  • If all the responders in the affected part of the responder chain are instances of your own subclass of UIView itself or of your own subclass of UIViewController, you will generally want to follow the simplest possible rule: implement all the touch methods together in one class, so that touches arrive at an instance either because it was the hit-test view or because it is up the responder chain from the hit-test view; and do not call super in any of them. In this way, “the buck stops here” — the touch handling for this object or for objects below it in the responder chain is bottlenecked into one well-defined place.

  • If you subclass a built-in UIView subclass and you override its touch handling, you don’t have to override every single touch method, but you do need to call super so that the built-in touch handling can occur.

  • Never call a touch method directly (except to call super).

  • Don’t allow touches to arrive from lower down the responder chain at an instance of a built-in UIView subclass that implements built-in touch handling, because such a class is completely unprepared for the first parameter of a touch method containing a touch not intended for itself. Judicious use of isUserInteractionEnabled or hit-test munging can be a big help here.

(I’m not saying, however, that you have to block all touches from percolating up the responder chain; on the contrary, it’s normal for unhandled touches to remain unhandled all the way to the top of the responder chain, with no harm done.)

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

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