12. Touch Basics

Gestures are everywhere. Tapping buttons, scrolling through lists, zooming images...most interactions with an iPhone or iPad are some sort of gesture. iOS provides lots of user interface elements with built-in gestures, but what if you want more?

That is what UIGestureRecognizer and related classes are for. They enable you to do things like attach a custom swipe or create your own recognizers. Setting up a recognizer can be as easy as writing an action method and making a few connections in Interface Builder (IB).

In this chapter, you add three recognizers to the CarValet app: One uses swipes to move through car details, another takes the iPad app back home, and the last one tracks a finger. First, you learn the basics of gesture recognizers—the common parts and the different types. Next, you implement swipes to navigate through car details. Then, you learn about creating a simple custom gesture recognizer and the importance of state. Next, you add a custom iPad gesture for resetting the interface. Finally, you create one more custom gesture for moving a view.

Gesture Recognizer Basics

From tapping buttons to moving a slider, many built-in interface elements implement their own gestures. But sometimes you need to do something different: swipe through cars, give a quick gesture to reset state, or move a view to a new destination. All these gestures are easy to implement using UIGestureRecognizer and related classes.

UIGestureRecognizer is an abstract superclass and encapsulates the properties and behaviors that are common to any recognizer: what view the recognizer is attached to, whether it is enabled, where the touch is happening, how many fingers are on the screen, and so on. Built-in recognizers as well as your own custom ones are all based on the abstract one.

Recognizers have a few common attributes. Every gesture has the following:

Image The view the gesture recognizer is attached to

Image The number of touches—both how many fingers are required and, during the gesture, how many are on the screen

Image The current or previous touch location(s)

Gestures can have target/action pairs. A target is an object that implements an action method. The gesture sends the action method at appropriate points. When the message is sent depends on the gesture type: single or continuous.

A single gesture happens once—for example, a tap or a swipe. The action is called when the user performs the gesture. Continuous gestures, such as pinches and pans, call the action method throughout the gesture as well as at the end. This is an important difference. With a single gesture, the action message is called at the end, and only if the gesture is recognized. The method is not called if the gesture is cancelled or unrecognized. Continuous gestures send a stream of updates, and for the built-in gestures, have no notion of successful recognition.

iOS comes with six recognizer classes ready for you to use:

Image UITapGestureRecognizer detects taps in a view. You specify how many touches, or fingers, are needed as well as how many times those touches need to happen. For example, you could require two fingers tapping three times. Tap is a single gesture, so the action message is sent only when the full gesture is recognized. Failed recognition sends nothing.

Image UIPinchGestureRecognizer is a continuous gesture activated when the user pinches two fingers together or pushes them apart. You get messages when the fingers move and have access to a scale factor and velocity. These let you perform an action in time with the user. The built-in pinch gesture uses the scale and velocity to zoom in and out in time with the user.

Image UIRotationGestureRecognizer happens as a user rotates two fingers. You get continuous messages with the amount of rotation as well as the velocity. You use these to match the onscreen rotation, or your own behavior, to user movement.

Image UISwipeGestureRecognizer looks for a swipe in the specified direction with the required number of fingers. A swipe is a single gesture going up, down, left, or right, so you get a message only if the gesture is completed.

Image UIPanGestureRecognizer starts when the user drags a particular number of fingers across the screen. You specify the minimum and maximum fingers and are continuously sent messages. The gesture includes the translation and velocity.

Image UILongPressGestureRecognizer starts when the required number of fingers are pressed for the specified time without moving beyond a pixel boundary. You get continuous messages as the gesture is in progress.

Swiping Through Cars

People expect gestures to be a part of apps, so much so that they try gestures in places where they think they should work. One common navigation gesture is swiping through detailed content. The car images view of the CarValet app already supports swipes, but the detail view does not.

Most of the work of adding swipe gestures is in the supporting code for changing the displayed car. Very little is specific to the swipe. These are the general steps to get swipes working:

1. Add a method for moving back and forth through the cars: nextOrPreviousCar: in CarTableViewController.

2. Enable both car detail controllers to call the new navigation method.

3. Add action selectors for the swipe gestures to the iPhone and iPad car detail controllers.

4. Use the Storyboard editor to add and connect right and left swipe gestures for each car detail controller.

Moving Through Cars

You need to add a method to move to the next or previous car. The hardest part is figuring out the next row, especially because there can be multiple sections. Next should go to the first car of the next section or wrap around to the beginning of the first section from the last car. Previous should go backward through the list of cars, moving to the last car when a section changes or to the last car from the first car.

Use the project you were using for Chapter 11, “Navigation Controllers II: Split View and the iPad,” or use CH12 CarValet Starter from the sample code from this chapter. First, add code for moving to the next or previous car. There are three routines to do this, shown in Listings 12-1, 12-2, and 12-3. You need to start by adding the following #pragma before the end of the class in CarTableViewController.m and then add each listing after the #pragma:

#pragma mark - View previous/next car

Listing 12-1 Finding the Next Car


- (NSIndexPath*)indexPathOfNext {
    NSInteger section = self.tableView.indexPathForSelectedRow.section;     // 1
    NSInteger row = self.tableView.indexPathForSelectedRow.row;
    NSInteger maxSection = [self.tableView numberOfSections] - 1;
    NSInteger maxRows = [self.tableView numberOfRowsInSection:section] - 1;

    if ((row + 1) > maxRows) {                                              // 2
        if (section > maxSection) {                                         // 3
            section = 0;
            row = 0;
        } else {
            if (section != maxSection) {                                    // 4
                section += 1;
            } else {
                section = 0;
            }
            row = 0;
        }
    } else {                                                                // 5
        row += 1;
    }

    return [NSIndexPath indexPathForRow:row inSection:section];
}


Here’s what happens in the numbered lines in Listing 12-1:

1. Set up state variables.

2. Determine whether the next row is beyond the end of the current section.

3. The row is beyond, so check whether this is the last section, and if so, reset to the first row of the first section.

4. This is not the last section, so go to the first row of the next section if there is one.

5. This is not the last row, so go to the next row (increment by 1).

Listing 12-2 Finding the Previous Car


- (NSIndexPath*)indexPathOfPrevious {
    NSInteger section = self.tableView.indexPathForSelectedRow.section;
    NSInteger row = self.tableView.indexPathForSelectedRow.row;
    NSInteger maxSection = [self.tableView numberOfSections] - 1;
    NSInteger maxRows = [self.tableView numberOfRowsInSection:section] - 1;

    if (row == 0) {                                                         // 1
        if (maxSection == 0) {                                              // 2
            row = maxRows;
        } else {
            if (section == 0) {                                             // 3
                section = maxSection;
            } else {
                section -= 1;
            }
            row = [self.tableView numberOfRowsInSection:section] - 1;       // 4
        }
    } else {
        row -= 1;                                                           // 5
    }

    return [NSIndexPath indexPathForRow:row inSection:section];
}


Here’s what happens in the numbered lines in Listing 12-2:

1. Determine whether this is the first row of the section.

2. This is the first row, so if there is one section, wrap the row to the end.

3. There is more than one section, so decrement or wrap the section number.

4. Set the row to the last item in the chosen section.

5. This is not the first row, so go to the previous one (decrement by 1).

Listing 12-3 Moving to the Next or Previous Car


- (void)nextOrPreviousCar:(BOOL)isNext {
    NSIndexPath *newSelection;                                              // 1

    if (isNext) {
        newSelection = [self indexPathOfNext];
    } else {
        newSelection = [self indexPathOfPrevious];
    }

    [self.tableView selectRowAtIndexPath:newSelection                       // 2
                                animated:YES
                          scrollPosition:UITableViewScrollPositionMiddle];

    if (self.delegate != nil) {                                             // 3
        NSIndexPath *previousPath = currentViewCarPath;

        [self.delegate selectCar:[self carToView]];                         // 4

        if (previousPath != nil) {
            [currentTableView reloadRowsAtIndexPaths:@[previousPath]
                                    withRowAnimation:NO];
        }
    }
}


Here’s what happens in the numbered lines in Listing 12-3:

1. Specify a variable for the index path of the new selected cell.

2. After finding the new path, tell the table view to show the new selection.

3. Check whether there is a delegate, like the iPad car detail.

4. If there is a delegate, update the delegate’s car and update the table view cell for the old car.

Calling nextOrPreviousCar:

The iPad and iPhone car detail controllers need to send the new nextOrPreviousCar: method to their related car table menu. One way to do that is to make the new method public and then set a reference to the car table view in each detail view. An alternative is to expand the existing ViewCarProtocol and add a protocol-enabled delegate to the iPad car detail. Add the protocol and behavior by following these steps:

1. Add a new protocol message to ViewCarProtocol.h with the same signature as nexOrPreviousCar:, using the following lines:

@optional
- (void)nextOrPreviousCar:(BOOL)isNext;

The message is optional because other classes support the protocol but might not need to move through cars. For example, MainMenuViewController on iPad supports the protocol but has no control over moving cars.

2. Open CarDetailViewController.h and import ViewCarProtocol.h.

3. Add a delegate that supports ViewCarProtocol:

@property (weak, nonatomic) id <ViewCarProtocol> delegate;

4. Open MainMenuViewController.m and make the bold changes in Listing 12-4 for tableView:didSelectRowAtIndexPath:.

Listing 12-4 Updates to tableView:didSelectRowAtIndexPath:


UIViewController *nextController;
CarTableViewController *carTable;                                               // 1

BOOL updateDetailController = YES;

switch (indexPath.row) {
    case kPadMenuCarsItem:
        carTable = [iPhoneStory instantiateViewControllerWithIdentifier:
                    @"CarTableViewController"];

        carTable.navigationItem.title = @"Cars";
        carTable.delegate = self;

        nextController = carTable;
        [self.navigationController pushViewController:nextController
                                             animated:YES];

        if (currentCarDetailController == nil) {
            currentCarDetailController = [[self storyboard]
                                        instantiateViewControllerWithIdentifier:
                                        @"CarDetailViewController"];
            [detailController setCurrDetailController:currentCarDetailController
                                          hidePopover:NO];
            currentCarDetailController.delegate = carTable;                     // 2
        }

        updateDetailController = NO;
        break;


Here’s what happens in the numbered lines in Listing 12-4:

1. Get a reference to the car table view controller, in case you need to create a detail view, and set its delegate.

2. Set the delegate of the iPad car detail view so it can send messages to move back and forth through the car detail view.

Adding Action Selectors

You now have almost all the code changes needed to support swipes: The car table can move to the next or previous car, and both the iPhone and iPad car detail views can call the new method. The last new code is the selectors used by the swipe gestures.

All the work for the iPad selectors is done by nextOrPreviousCar:, which contains code for updating the state of the delegate—in this case, the iPad car detail view. Add the code in Listing 12-5 above the utility method #pragma in CarDetailViewController.m.

Listing 12-5 iPad Car Detail Swipe Gesture Recognizers


#pragma mark - Gestures

- (IBAction)swipeCarRight:(UISwipeGestureRecognizer*)sender {               // 1
    [self.delegate nextOrPreviousCar:YES];                                  // 2
}

- (IBAction)swipeCarLeft:(UISwipeGestureRecognizer*)sender {                // 3
    [self.delegate nextOrPreviousCar:NO];                                   // 4
}


Here’s what happens in the numbered lines in Listing 12-5:

1. Add an action method for the swiping right gesture. Note that you can quickly create any action method by using Xcode’s completion feature. See the “Tip: Quickly Adding an Action Method.”

2. Send YES to go to the next car.

3. Add an action method for swiping left.

4. Send NO for the previous car.


Tip: Quickly Adding an Action Method

You can quickly add action messages to your class files by using Xcode completion and the built-in templates. Here’s what you do:

1. Place your cursor at the start of an empty line between the @implementation and @end statements, and outside the body of any method.

2. Type a dash (-), followed by an open parenthesis (() and then start typing ibaction. As you type, Xcode provides completions.

3. When you see a completion like the one in Figure 12-1, press Return.

Now you can fill in the selector and, if needed, the type of the selector. You can also change the argument name to make the code self-documenting.

Image

Figure 12-1 Xcode action method completion


Implementing the iPhone Action Selectors

Setting up the action selectors for ViewCarTableViewController in the iPhone detail view requires a bit of work. Because the car is set by car detail using a protocol method, you cannot use a setter to force an update. And, the delegate needs to be told if the current car needs to be saved.

This is the basic selector flow:

1. Tell the delegate to save the current car.

2. Move to the next or previous car.

3. Update the view content with the new car.

Make the following changes in ViewCarTableViewController.m:

1. Add the following skeleton method above titleText (the new code is bold):

...
#pragma mark - Utility Methods

- (void)loadCarData {
}


#pragma mark - MakeModelEditProtocol

-(NSString*)titleText {
...

2. In viewDidLoad, select all the code except the call to [super viewDidLoad] and then cut and paste that code into the body of loadCarData.

3. Add a call to the new loadCarData method in viewDidLoad. Now viewDidLoad has only two lines of code.

4. Put the code from Listing 12-6 above the Utility Methods #pragma methods you just added.

Listing 12-6 iPhone Car Detail Swipe Gesture Recognizers


#pragma mark - Gestures

- (IBAction)swipeCarRight:(UISwipeGestureRecognizer*)sender {               // 1
    [self.delegate carViewDone:dataUpdated];                                // 2

    [self.delegate nextOrPreviousCar:YES];                                  // 3

    [self loadCarData];                                                     // 4
}

- (IBAction)swipeCarLeft:(UISwipeGestureRecognizer*)sender {                // 5
    [self.delegate carViewDone:dataUpdated];

    [self.delegate nextOrPreviousCar:NO];                                   // 6

    [self loadCarData];
}


Here’s what happens in the numbered lines in Listing 12-6:

1. Add an action method for the swiping right gesture.

2. Tell the delegate to update the current car.

3. Send YES to go to the next car.

4. Update the onscreen content.

5. Add an action method for swiping left.

6. Show the previous car by sending NO.

Adding the Swipe Gestures

You have written all the code for reacting to gestures. You now have a method for moving to the next or previous car, both car detail views can send the message, and there are selectors for the gestures.

The final step is to add the gestures and wire them in. You can do all this in the Storyboard editor for the iPhone and iPad. The steps are the same for either platform. The book shows iPhone and lets you update iPad for practice. Follow these steps:

1. Open Main_iPhone.storyboard.

2. Look for the Swipe gesture object in the Utilities area, or type Swipe into the Search area. Drag one into the bar below the View Car controller on the IB Canvas as shown in Figure 12-2. (The image uses a different search term to show all the gesture objects.)

Image

Figure 12-2 Adding a gesture recognizer

3. In the left-hand list, add Right to the end of the name.

As shown in Figure 12-3, set the action method for the swipe by Ctrl-dragging from the new swipe object to the view controller object. Then choose swipeCarRight: in the popup that appears.

Image

Figure 12-3 Setting the gesture selector

4. Open the connection popup for the table view and connect the gestureRecognizers outlet collection to the new recognizer.

5. Drag in another swipe gesture, but this time drag it onto the table view, as shown in Figure 12-4. Doing this adds the swipe to the table view’s collection of recognizers.

Image

Figure 12-4 Adding a gesture to the table view

6. Set the selector for the new gesture to swipeCarLeft:.

7. With the gesture still selected, use the Attributes inspector on the left to set the swipe direction to Left, as shown in Figure 12-5.

Image

Figure 12-5 Setting the gesture direction

Choose an iPhone simulator and run the app. Make sure there are a few cars and then view the detail for one of them. Now you can swipe right to go to the next car or left to go back.

You can use similar steps to add swipes to the iPad. You should add the swipe objects to CarDetailViewController in the iPad storyboard. When you are done, choose an iPad simulator and run the app again. Try it in landscape to see both the menu of cars and car detail. Swiping right and left in the detail area should change the car detail and updates the highlighted cell in the table. Try different table sorts (such as make or model) and make sure the selected car moves between sections as expected.

Preventing Recognizers from Working

The iPad occasionally shows a car detail view with no information and disables interacting with any of the buttons, fields, or the picker. It does not disable the swipes. They do work, but the behavior might be unexpected.

You can disable recognizers in two ways. The first is to set a recognizer’s enabled property to NO. Doing so requires a reference to the gesture. A second way is to set a gesture delegate and implement gestureRecognizer:shouldReceiveTouch:, as follows:

1. Open Main_iPad.storyboard and set the delegate for the swipe gestures to CarDetailViewController. You can do this by Ctrl-clicking a gesture recognizer and dragging a connection from the delegate outlet to the controller.

2. Open CarDetailViewController.m and add the following code after the last gesture action method. The method only allows gesture recognizers to receive touches if there is a car (that is, if myCar is non-nil):

-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
      shouldReceiveTouch:(UITouch *)touch {
    return (self.myCar != nil);
}

Run the app and confirm that gestures are blocked when there are no car details shown and that gestures work when there is a car.

Custom Recognizers

In the last section, you added a built-in recognizer to the CarValet app. The next step is to add a custom recognizer. Before doing that, you need to understand more about how recognizers work.

Recognizers are state machines, flowing through four states for single-gesture recognizers and through seven for continuous ones. You update state and implement the gesture behavior by specializing one or more UIGestureRecognizer messages.

Recognizer States

Gesture recognizers are in one of four states—or seven, if they are continuously sending updates. Their default state is that the gesture is possible, even if there are no fingers on the screen. This might seem strange, but it is very important. When fingers touch down, the system only checks gestures that could recognize something (and are enabled). Therefore, UIGestureRecognitionPossible is the default state for all recognizers.

Single gestures track touch(es) until the gesture is either recognized, not possible to recognize, or cancelled. Continuous gestures add states for sending messages to their delegate and for resetting to some default state. Figure 12-6 shows the two different flows.

Image

Figure 12-6 Stroke state diagrams

The top flow shows a single-gesture recognizer, and it is in one of three states. Possible is the default, and you are responsible for setting Recognized when appropriate. The Failed state occurs when the wrong number of touches occurs, the stroke pattern is wrong, the user lifts their finger(s) before the stroke is detected, or some other interruption occurs. The system resets the state to Possible after you finish processing a gesture.

Continuous flow is a bit more complicated. Failure usually occurs only for the wrong number of touches, although Cancellation takes care of the other failure cases. Unlike with a single stroke, the recognizer moves through a Began into a repeating Changed state. Began and Changed result in updates to the delegate. If there is an ending pattern, when it is detected, the state changes to either Recognized or, if the gesture was not close enough to the required one, Failed. The Failed state also occurs when there is a system interruption or the user lifts his or her finger before the gesture is completed.

Table 12-1 shows the system state constants and briefly describes them.

Image

Table 12-1 Gesture State Constants

Specializing Recognizer Messages

You can create a custom recognizer by subclassing UIGestureRecognizer and implementing one or more of the touch event handlers. You are also likely to implement other methods from the superclass, as well as your own logic and state variables.

The recognizer has two main jobs: Keep the gestures state up to date and recognize your custom stroke and/or perform your custom behavior. The main event handler methods follow the sequence of a stroke:

Image touchesBegan:withEvent: is sent when the touches first occur—that is, when the user touches the screen.

Image touchesMoved:withEvent: is sent each time the user moves one or more fingers.

Image touchesEnded:withEvent: means the user has lifted his or her finger(s).

Image touchesCancelled:withEvent: is an interruption by the system, such as a phone call. This event is not the same as the Cancelled state, though it might result in the gesture moving to that state. For example, if a drag is interrupted, you could reset the position or leave it where it was.

Image reset is called by the system to reset the state of your recognizer. Typically, this is sent when the state changes to Recognized or End.

There are additional methods in both UIGestureRecognizer and UIGestureRecognizerDelegate for helping to coordinate with other gesture recognizers. For more information, see Chapter 1, “Gestures and Touches,” in Core iOS 6 Developers Cookbook by Erica Sadun.

A confusing part of creating recognizers is that you do not end your own gesture. In other words, setting the state to Recognized does not stop the flow of events to touchesMoved:withEvent:, nor does it call touchesEnded:withEvent:. Instead, your recognizer might need some internal state variables to track whether the gesture is done. The only things that can stop the flow of touches are the user removing their finger(s) from the display or the system cancelling the gesture. Typically, you wait until touchesEnded to set the Recognized or Failed state. The best way to see how this works is to implement a recognizer, which you do in the next section.

iPad Go Home

Tapping a tab bar button in CarValet on the iPhone takes you back to the root view of that particular section. If you are viewing car details, tapping the tab button takes you back to the car table menu. On iPad, there is no similar control. Sometimes it is useful to set the iPad app back to a default state.

In this section, you implement a custom gesture recognizer for returning the iPad to its just-launched state: The master view shows the app sections menu, and the detail view has the default car valet tent picture.

For a gesture to work, the recognizer needs to be attached to a view. When the user taps in that view or any of its subviews, the system sends appropriate messages. The go home recognizer is attached to the current detail view. Since it makes no sense to activate the gesture until the main menu is up, MainMenuViewController sets up the recognizer and is the delegate. DetailController takes care of adding the gesture to the current detail view, if needed.

There are three steps for implementing and attaching the recognizer:

1. Create the custom ReturnGestureRecognizer subclass.

2. Add the gesture recognizer in DetailController.

3. Add a gesture callback action in MainMenuViewController and add the gesture recognizer to DetailController.

Creating the Return Gesture Recognizer

Before you create the gesture recognizer, you need to choose a gesture and determine how to detect it. Since it returns the iPad app to a known state, you can use a small letter r. The first challenge is determining how to recognize the character.

The easiest way to recognize a stroke is by breaking it down into components separated by transitions in x and/or y movement. As part of the events, you get both the current and previous touch points, allowing detection of changes in movement. The x value increases from left to right of the screen, and y increases from top to bottom. You can use comparisons between previous and current positions to determine changes in direction. If you needed other information such as velocity, you could use other data, such as time.

An r has two strokes, with four main components, as shown in Figure 12-7. The left side shows a completed lowercase r. The middle image is the two main stroke movements: a downward straight line followed by an upward-right hook.

Image

Figure 12-7 The r stroke

The right-side image breaks the gesture down into strokes and shows the three main transitions. The first stroke is downward and starts the recognizer. The first transition happens when y reverses direction after moving down some minimal distance. Next, x increases and y decreases. Then x increases, and y moves relatively little. When this last phase happens, the stroke is recognized.

Creating ReturnGestureRecognizer

Before you start coding, you need to design any state variables. One obvious variable is the current stroke phase: Down is 0, the initial up is 1, the first turn is 2, and the final stroke is 3. The other variable comes from the need to have a minimal amount of down stroke. Tracking that requires knowing the initial touch. Implement the gesture recognizer by following these steps:

1. Use the Navigator to add a new group below iPad called Other. Select that group and add a new Objective-C class called ReturnGestureRecognizer, a subclass of UIGestureRecognizer.

2. Import the gesture recognizer subclass into ReturnGestureRecognizer.m:

#import <UIKit/UIGestureRecognizerSubclass.h>

3. Add a #define for the maximum stroke wobble and add the state variables to the implementation (the new code is bold):

#define kRStrokeDelta       5.0f

@implementation ReturnGestureRecognizer {
    NSInteger   strokePhase;
    CGPoint     firstTap;
}

4. Use Listings 12-7, 12-8, and 12-9 to add the recognition methods.

Listing 12-7 Return Gesture Methods for Reset and Began


- (void)reset {
    [super reset];                                                          // 1

    strokePhase = 0;                                                        // 2
}


- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    [super touchesBegan:touches withEvent:event];

    if (([touches count] != 1) ||
         ([[touches.anyObject view] isKindOfClass:[UIControl class]])) {    // 3
        self.state = UIGestureRecognizerStateFailed;
        return;
    }

    firstTap = [touches.anyObject locationInView:self.view.superview];      // 4
}


Here’s what happens in the numbered lines in Listing 12-7:

1. Always call the superclass.

2. Reset the stroke phase.

3. If there is more than one touch/finger or the touch is in a control such as a button, the gesture fails.

4. There is only one finger, so set the location of the first tap.

Listing 12-8 The Main touchesMoved:withEvent: Method


- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    [super touchesMoved:touches withEvent:event];

    if ((self.state == UIGestureRecognizerStateFailed) ||                   // 1
        (self.state == UIGestureRecognizerStateRecognized)) {
        return;
    }

    UIView *superView = [self.view superview];                              // 2
    CGPoint currPoint = [touches.anyObject locationInView:superView];
    CGPoint prevPoint = [touches.anyObject previousLocationInView:superView];

    if ((strokePhase == 0) &&                                               // 3
        ((currPoint.y - firstTap.y) > 10.0) &&
        (currPoint.y <= prevPoint.y)) {

        strokePhase = 1;

    } else if ((strokePhase == 1) &&                                        // 4

               ((currPoint.x - prevPoint.x) >= kRStrokeDelta)) {

        strokePhase = 2;

    } else if ((strokePhase == 2) &&                                        // 5
               ((currPoint.y - prevPoint.y) <= kRStrokeDelta) &&
               (currPoint.x > prevPoint.x)) {

        strokePhase = 3;                                                    // 6
        self.state = UIGestureRecognizerStateRecognized;
    }
}


Here’s what happens in the numbered lines in Listing 12-8:

1. If stroke detection has failed or is recognized, return. Remember, this method is called as long as there are fingers on the screen, even if the gesture is recognized.

2. Get the current and previous touch points in the same view coordinate system used by firstTap.

3. Check that the downward stroke is at least 10.0 points and then transitions to an upward stroke when the current y is less than the previous y.

4. Check for start of the stroke up and to the right, making sure the change in x is not too big.

5. The final stroke phase happens when x increases without too much movement in y.

6. The stroke is recognized, so set the phase and stroke state.

Listing 12-9 Return Gesture Method for Cancelled


- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
    [super touchesCancelled:touches withEvent:event];

    self.state = UIGestureRecognizerStateFailed;                            // 1
}


Here’s what happens in the numbered line in Listing 12-9:

1. A system interruption sets the stroke state to Failed.

Adding the Gesture Recognizer to the Current Detail

Any time any detail view except the default one is shown, the return gesture recognizer is added. And even if it is accidentally added to the default detail view, the worst that can happen is returning the app to the default state, showing that same detail view.

DetailController manages showing detail content, so it is the logical place to add the gesture recognizer. However, it is not the best controller for responding to a recognized gesture. Although DetailController manages the detail, it does not manage the overall state of the app. That is done by MainMenuViewController, so this is where the recognized gesture action method goes.

You need to make a few small additions to the .h file and to setCurrDetailController:

1. Open DetailController.h, and add a public property to the interface:

@property (strong, nonatomic) UIGestureRecognizer *returnGesture;

2. Open DetailController.m, import ReturnGestureRecognizer.h, and add the lines in bold to setCurrDetailController:hidePopover:. The code checks whether there is a return gesture and, if there is, adds one to any new detail controller:

...
NSArray *newStack = nil;

if (self.returnGesture && currDetailController &&
    (_currDetailController != currDetailController)) {
    [currDetailController.view addGestureRecognizer:self.returnGesture];
}

if (currDetailController == nil) {
...

Creating and Responding to the Gesture Recognizer

All you have to do now is add the action method, create the gesture recognizer, and add it to the detail controller. Follow these steps:

1. Import ReturnGestureRecognizer.h into MainMenuViewController.m.

2. Add the code in Listing 12-10 below carViewDone:.

3. Update viewDidLoad to allocate a gesture recognizer and then set the detail controller’s returnGesture property. Add the following code to the end of the method:

ReturnGestureRecognizer *returnGesture = [[ReturnGestureRecognizer alloc]
                                          initWithTarget:self
                                          action:@selector(returnHome:)];

[DetailController sharedDetailController].returnGesture = returnGesture;

Make sure the recognizer sets the delegate to the main menu view controller and uses the action method you added in step 2.

Listing 12-10 Responding to a Recognized Return Gesture


#pragma mark - Return Gesture Action Method

- (IBAction)returnHome:(UIGestureRecognizer*)sender {
    [self.tableView                                                         // 1
     deselectRowAtIndexPath:self.tableView.indexPathForSelectedRow
     animated:YES];

    if (currentCarDetailController != nil) {                                // 2
        [self.navigationController popToRootViewControllerAnimated:YES];
        currentCarDetailController = nil;
    }

    [DetailController sharedDetailController].currDetailController = nil;   // 3
}


Here’s what happens in the numbered lines in Listing 12-10:

1. Deselect any highlighted cell in the main menu view table.

2. If there is another master menu showing, such as the car list, go back to the main menu.

3. Set the detail view to the picture of the valet tent.

Run the app on an iPad simulator, navigate to the About screen, and use your finger to make an r onscreen. Remember both the down and up portions of the stroke. When the stroke is recognized, the app should return to the initial state. Try this on other screens. Note that in the images scene, you might need to start the gesture in the upper area, outside of the car image. If you find that things are not working, make sure the code is entered correctly, and if needed, try debugging the stroke. See the “Tip: Debugging Strokes.”


Tip: Debugging Strokes

Debugging strokes can be really difficult. You cannot set a typical breakpoint as that would interrupt the stroke. The best idea is to use NSLog statements in the various parts of the recognition routine, especially the state transition portions of touchesMoved:withEvent:.

There are even better ways, though, using more advanced breakpoint features. See Chapter 14, “Instruments and Debugging,” for more information.


One More Gesture

A chapter on gestures wouldn’t be complete without information on dragging a view around the screen. This section gives a bit more practice with custom recognizers. You add a custom drag view gesture and a yellow car that you can drag around the About screen. See Challenge 3 at the end of this chapter for another way to drag views using a system gesture.

Drag Gesture Recognizer

Dragging a view is a continuous recognizer. The action method is called repeatedly as long as the finger is down and the recognizer is in an appropriate state. As explained earlier in this chapter, in the section “Custom Recognizers,” continuous recognizers have more states. Create your own continuous recognizer for dragging a view by following these steps:

1. Select the Other group in the Navigator and add the new class DragViewGesture, based on UIGestureRecognizer.

2. Import the gesture recognizer subclass:

#import <UIKit/UIGestureRecognizerSubclass.h>

3. Add the methods shown here:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    [super touchesBegan:touches withEvent:event];

    if ([touches count] != 1) {
        return;
    }

    self.state = UIGestureRecognizerStateBegan;
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    self.state = UIGestureRecognizerStateRecognized;
}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
    self.state = UIGestureRecognizerStateRecognized;
}

There are two big differences from the previous custom recognizer. First, setting the state to UIGestureRecognizerStateBegan in touchesBegan: causes a callback to the target’s action method. Second, touchesCancelled:withEvent: is added.

4. Add touchesMoved:withEvent: from Listing 12-11.

Listing 12-11 Drag View touchesMoved:withEvent:


- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    [super touchesMoved:touches withEvent:event];

    if ((self.state == UIGestureRecognizerStateFailed) ||                   // 1
        (self.state == UIGestureRecognizerStateRecognized)) {
        return;
    }

    CGPoint currPoint = [touches.anyObject                                  // 2
                         locationInView:self.view.superview];
    CGPoint prevPoint = [touches.anyObject
                         previousLocationInView:self.view.superview];

    CGRect newRect = CGRectOffset(self.view.frame,                          // 3
                                  currPoint.x - prevPoint.x,
                                  currPoint.y - prevPoint.y);

    if (CGRectContainsRect(self.view.superview.frame,                       // 4
                           newRect)) {
        self.view.frame = newRect;
    }

    self.state = UIGestureRecognizerStateChanged;                           // 5
}


Here’s what happens in the numbered lines in Listing 12-11:

1. Continue dragging only if the gesture is valid.

2. Get the current and previous points.

3. Create a new view frame by offsetting the current frame by the change in x and y.

4. Update the view frame only if it is completely inside the containing view.

5. Set the state to UIGestureRecognizerStateChanged so the target’s action method is called in the next event loop.

In reality, there is no need to call back to a target. All the code to move a view is inside the drag gesture. The changes in gesture state that enable callbacks are shown to illustrate where such callbacks typically go.

Adding the Taxi View with Drag

Follow these steps to enable dragging the car view:

1. Open AboutViewController.xib in IB.

2. Add an image view above the text and set image to placeholder and the background color to lemon.

3. While you are in the XIB, fix the centering of the existing text so it works on the iPad. Select the label and set the constraints to horizontally and vertically centered in the container.

Now set the taxi view constraints to the following: The bottom is the system distance from the top of the label, the width and height are fixed at 40, and it is horizontally centered in the superview.

4. Select the image view and check User Interaction Enabled in the Attributes inspector. This flag enables the image view to receive gestures. Without it, placing a finger in the view does nothing.

5. Open the Assistant editor and create an IBOutlet to the new view called taxiView.

6. Open AboutViewController.m and import DragViewGesture.h.

7. Add the following lines of code to viewDidLoad to create a new drag view gesture and add it to the taxi view:

DragViewGesture *dragView = [[DragViewGesture alloc] init];
[self.taxiView addGestureRecognizer:dragView];

Run the app on iPhone and open the About tab. Note that if your app appears to crash, it is due to conflicting layout constraints. You will see something like this in the console:

When the app is running, you can touch the taxi view and drag it around the screen but not off the screen (though it is possible for the taxi to get stuck under the navigation bar, see Challenge 4). Now run the app on iPad and open About. Again, you can drag the taxi view around the screen but not off the screen. The use of CGRectContainsRect in touchesMoved:withEvent: forces all sides of the taxi view to stay inside its container.

Summary

In this chapter, you added gestures to the CarValet app. First, you learned about the basics of gesture recognizers, including ones provided by the system. Next, you added built-in swipes to move back and forth through the details for each car. You learned some shortcuts when adding gestures in the storyboard, as well as how to disable gesture recognizers.

Then you learned how to create custom recognizers, including details on how they work. Next, you applied your knowledge by implementing a custom recognizer to reset the state of the iPad app. While doing this, you learned a way to recognize more complex gestures by breaking them down into components and transitions.

Finally, you created a bonus continuous recognizer to drag a yellow taxi around the About screen. Along the way, you saw a quick way to create action methods in Xcode, as well as how to constrain a view to moving inside its superview.

You can use recognizers to add features that both engage and delight the customer. System gestures let you trigger app behaviors using swipes, taps, drags, and more. Custom gestures enable you to offer ways for the user to easily and naturally accomplish tasks.

Chapter 13, “Introducing Blocks,” introduces blocks, a powerful extension of Objective-C and C++. You learn how to create blocks of code that are commonly used for method arguments. As you will see, blocks are very powerful, carrying with them properties and variables available in the environment where they were created.

Challenges

1. On iPhone, add a gesture to go back from the car detail view to the car table menu. The gesture should be a triple tap, using three fingers for each tap. Use a built-in recognizer.

2. Add two different ways to reset the taxi in AboutViewController. First, reset the car to its original position if the user puts a second finger on the screen. Resetting the car should also cancel the gesture so the taxi no longer moves, even if the user keeps moving a finger on the screen. Second, add a reset car view to AboutViewController. Dragging the car over this view should reset it and cancel the gesture.

3. Move the view in AboutViewController using UIPanGestureRecgonizer instead of the custom DragViewGesture. The pan gesture gives you more information, such as the translation amount. You need to determine how to use that to move the yellow taxi.

4. It is possible for the taxi view to get stuck under the navigation bar. This happens when you drag the taxi under the translucent navigation bar, and the notifications panel comes down. The best way to prevent this is to stop the taxi view from going under the navigation bar.

Modify the code from Listing 12-11 so the first argument in the call to CGRectContainsRect also excludes the navigation bar from the superview’s frame.