Motion Events

Users generate motion events when they move, shake, or tilt the device. These motion events are detected by the device hardware, specifically, the accelerometer and the gyroscope.

The accelerometer is actually made up of three accelerometers, one for each axis—x, y, and z. Each one measures changes in velocity over time along a linear path. Combining all three accelerometers lets you detect device movement in any direction and get the device’s current orientation. Although there are three accelerometers, the remainder of this document refers to them as a single entity. The gyroscope measures the rate of rotation around the three axes.

All motion events originate from the same hardware. There are several different ways that you can access that hardware data, depending on your app’s needs:

Getting the Current Device Orientation with UIDevice

Use the methods of the UIDevice class when you need to know only the general orientation of the device and not the exact vector of orientation. Using UIDevice is simple and doesn’t require you to calculate the orientation vector yourself.

Before you can get the current orientation, you need to tell the UIDevice class to begin generating device orientation notifications by calling the beginGeneratingDeviceOrientationNotifications method. This turns on the accelerometer hardware, which may be off to conserve battery power. Listing 4-1 demonstrates this in the viewDidLoad method.

After enabling orientation notifications, get the current orientation from the orientation property of the UIDevice object. If you want to be notified when the device orientation changes, register to receive UIDeviceOrientationDidChangeNotification notifications. The device orientation is reported using UIDeviceOrientation constants, indicating whether the device is in landscape mode, portrait mode, screen-side up, screen-side down, and so on. These constants indicate the physical orientation of the device and don’t necessarily correspond to the orientation of your app’s user interface.

When you no longer need to know the orientation of the device, always disable orientation notifications by calling the UIDevice method, endGeneratingDeviceOrientationNotifications. This gives the system the opportunity to disable the accelerometer hardware if it’s not being used elsewhere, which preserves battery power.

Listing 4-1  Responding to changes in device orientation

-(void) viewDidLoad {
     // Request to turn on accelerometer and begin receiving accelerometer events
     [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
     [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(orientationChanged:) name:UIDeviceOrientationDidChangeNotification object:nil];
}
 
- (void)orientationChanged:(NSNotification *)notification {
     // Respond to changes in device orientation
}
 
-(void) viewDidDisappear {
     // Request to stop receiving accelerometer events and turn off accelerometer
     [[NSNotificationCenter defaultCenter] removeObserver:self];
     [[UIDevice currentDevice] endGeneratingDeviceOrientationNotifications];
}
 

For another example of responding to UIDevice orientation changes, see the AlternateViews sample code project.

Detecting Shake-Motion Events with UIEvent

When users shake a device, iOS evaluates the accelerometer data. If the data meets certain criteria, iOS interprets the shaking gesture and creates a UIEvent object to represent it. Then, it sends the event object to the currently active app for processing. Note that your app can respond to shake-motion events and device orientation changes at the same time.

Motion events are simpler than touch events. The system tells an app when a motion starts and stops, but not when each individual motion occurs. And, motion events include only an event type (UIEventTypeMotion), event subtype (UIEventSubtypeMotionShake), and timestamp.

Designating a First Responder for Motion Events

To receive motion events, designate a responder object as the first responder. This is the responder object that you want to handle the motion events. Listing 4-2 shows how a responder can make itself the first responder.

Listing 4-2  Becoming first responder

- (BOOL)canBecomeFirstResponder {
    return YES;
}
 
- (void)viewDidAppear:(BOOL)animated {
    [self becomeFirstResponder];
}

Motion events use the responder chain to find an object that can handle the event. When the user starts shaking the device, iOS sends the first motion event to the first responder. If the first responder doesn’t handle the event, it progresses up the responder chain. See “The Responder Chain Follows a Specific Delivery Path” for more information. If a shaking-motion event travels up the responder chain to the window without being handled and the applicationSupportsShakeToEdit property of UIApplication is set to YES (the default), iOS displays a sheet with Undo and Redo commands.

Implementing the Motion-Handling Methods

There are three motion-handling methods: motionBegan:withEvent:, motionEnded:withEvent:, and motionCancelled:withEvent:. To handle motion events, you must implement either the motionBegan:withEvent: method or the motionEnded:withEvent: method, and sometimes both. A responder should also implement the motionCancelled:withEvent: method to respond when iOS cancels a motion event. An event is canceled if the shake motion is interrupted or if iOS determines that the motion is not valid after all—for example, if the shaking lasts too long.

Listing 4-3 is extracted from the sample code project, GLPaint. In this app, the user paints on the screen, and then shakes the device to erase the painting. This code detects whether a shake has occurred in the motionEnded:withEvent: method, and if it has, posts a notification to perform the shake-to-erase functionality.

Listing 4-3  Handling a motion event

- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event {
    if (motion == UIEventSubtypeMotionShake)
    {
        // User was shaking the device. Post a notification named "shake."
        [[NSNotificationCenter defaultCenter] postNotificationName:@"shake" object:self];
     }
}

Setting and Checking Required Hardware Capabilities for Motion Events

Before your app can access device-related features, such as accelerometer data, you must add a list of required capabilities to your app. Specifically, you add keys to your app’s Info.plist file. At runtime, iOS launches your app only if the device has the required capabilities. For example, if your app relies on gyroscope data, list the gyroscope as required so that your app doesn’t launch on devices without a gyroscope. The App Store also uses this list to inform users so that they can avoid downloading apps that they can’t run.

Declare your app’s required capabilities by adding keys to your app’s property list. There are two UIRequiredDeviceCapabilities keys for motion events, based on hardware source:

You can use either an array or a dictionary to specify the key-values. If you use an array, list each required feature as a key in the array. If you use a dictionary, specify a Boolean value for each required key in the dictionary. In both cases, not listing a key for a feature indicates that the feature is not required. For more information, see “UIRequiredDeviceCapabilities” in Information Property List Key Reference.

If the features of your app that use gyroscope data are not integral to the user experience, you might want to allow users with non-gyroscope devices to download your app. If you do not make gyroscope a required hardware capability, but still have code that requests gyroscope data, you need to check whether the gyroscope is available at runtime. You do this with the gyroAvailable property of the CMMotionManager class.

Capturing Device Movement with Core Motion

The Core Motion framework is primarily responsible for accessing raw accelerometer and gyroscope data and passing that data to an app for handling. Core Motion uses unique algorithms to process the raw data it collects, so that it can present more refined information. This processing occurs on the framework’s own thread.

Core Motion is distinct from UIKit. It is not connected with the UIEvent model and does not use the responder chain. Instead, Core Motion simply delivers motion events directly to apps that request them.

Core Motion events are represented by three data objects, each encapsulating one or more measurements:

The CMMotionManager class is the central access point for Core Motion. You create an instance of the class, specify an update interval, request that updates start, and handle motion events as they are delivered. An app should create only a single instance of the CMMotionManager class. Multiple instances of this class can affect the rate at which an app receives data from the accelerometer and gyroscope.

All of the data-encapsulating classes of Core Motion are subclasses of CMLogItem, which defines a timestamp so that motion data can be tagged with a time and logged to a file. An app can compare the timestamp of motion events with earlier motion events to determine the true update interval between events.

For each of the data-motion types described, the CMMotionManager class offers two approaches for obtaining motion data:

Pull is the recommended approach for most apps, especially games. It is generally more efficient and requires less code. Push is appropriate for data-collection apps and similar apps that cannot miss a single sample measurement. Both approaches have benign thread-safety effects; with push, your block executes on the operation-queue’s thread whereas with pull, Core Motion never interrupts your threads.

Always stop motion updates as soon as your app finishes processing the necessary data. As a result, Core Motion can turn off motion sensors, which saves battery power.

Choosing a Motion Event Update Interval

When you request motion data with Core Motion, you specify an update interval. You should choose the largest interval that meets your app’s needs. The larger the interval, the fewer events are delivered to your app, which improves battery life. Table 4-1 lists some common update frequencies and explains what you can do with data generated at that frequency. Few apps need acceleration events delivered 100 times a second.

Table 4-1  Common update intervals for acceleration events

Event frequency (Hz)

Usage

10–20

Suitable for determining a device’s current orientation vector.

30–60

Suitable for games and other apps that use the accelerometer for real-time user input.

70–100

Suitable for apps that need to detect high-frequency motion. For example, you might use this interval to detect the user hitting the device or shaking it very quickly.

You can set the reporting interval to be as small as 10 milliseconds (ms), which corresponds to a 100 Hz update rate, but most app operate sufficiently with a larger interval.

Handling Accelerometer Events Using Core Motion

The accelerometer measures velocity over time along three axes, as shown in Figure 4-1. With Core Motion, each movement is captured in a CMAccelerometerData object, which encapsulates a structure of type CMAcceleration.

Figure 4-1  The accelerometer measures velocity along the x, y, and z axes

To start receiving and handling accelerometer data, create an instance of the CMMotionManager class and call one of the following methods:

Listing 4-4 is extracted from the MotionGraphs sample code project, which you can examine for more context. In this app, the user moves a slider to specify an update interval. The startUpdatesWithSliderValue: method uses the slider value to compute the new update interval. Then, it creates an instance of the CMMotionManager class, checks to make sure that the device has an accelerometer, and assigns the update interval to the motion manager. This app uses the push approach to retrieve accelerometer data and plot it on a graph. Note that it stops accelerometer updates in the stopUpdates method.

Listing 4-4  Accessing accelerometer data in MotionGraphs

static const NSTimeInterval accelerometerMin = 0.01;
 
- (void)startUpdatesWithSliderValue:(int)sliderValue {
 
     // Determine the update interval
     NSTimeInterval delta = 0.005;
     NSTimeInterval updateInterval = accelerometerMin + delta * sliderValue;
 
     // Create a CMMotionManager
     CMMotionManager *mManager = [(APLAppDelegate *)[[UIApplication sharedApplication] delegate] sharedManager];
     APLAccelerometerGraphViewController * __weak weakSelf = self;
 
     // Check whether the accelerometer is available
     if ([mManager isAccelerometerAvailable] == YES) {
          // Assign the update interval to the motion manager
          [mManager setAccelerometerUpdateInterval:updateInterval];
          [mManager startAccelerometerUpdatesToQueue:[NSOperationQueue mainQueue] withHandler:^(CMAccelerometerData *accelerometerData, NSError *error) {
               [weakSelf.graphView addX:accelerometerData.acceleration.x y:accelerometerData.acceleration.y z:accelerometerData.acceleration.z];
               [weakSelf setLabelValueX:accelerometerData.acceleration.x y:accelerometerData.acceleration.y z:accelerometerData.acceleration.z];
          }];
     }
 
     self.updateIntervalLabel.text = [NSString stringWithFormat:@"%f", updateInterval];
}
 
 
- (void)stopUpdates {
     CMMotionManager *mManager = [(APLAppDelegate *)[[UIApplication sharedApplication] delegate] sharedManager];
     if ([mManager isAccelerometerActive] == YES) {
          [mManager stopAccelerometerUpdates];
     }
}

Handling Rotation Rate Data

A gyroscope measures the rate at which a device rotates around each of the three spatial axes, as shown in Figure 4-2.

Figure 4-2  The gyroscope measures rotation around the x, y, and z axes

Each time you request a gyroscope update, Core Motion takes a biased estimate of the rate of rotation and returns this information in a CMGyroData object. CMGyroData has a rotationRate property that stores a CMRotationRate structure, which captures the rotation rate for each of the three axes in radians per second. Note that the rotation rate measured by a CMGyroData object is biased. You can get a much more accurate, unbiased measurement by using the CMDeviceMotion class. See “Handling Processed Device Motion Data” for more information.

When analyzing rotation-rate data—specifically, when analyzing the fields of the CMRotationMatrix structure—follow the “right-hand rule” to determine the direction of rotation, as shown in Figure 4-2. For example, if you wrap your right hand around the x-axis such that the tip of the thumb points toward positive x, a positive rotation is one toward the tips of the other four fingers. A negative rotation goes away from the tips of those fingers.

To start receiving and handling rotation-rate data, create an instance of the CMMotionManager class and call one of the following methods:

  • startGyroUpdates—the pull approach

    After you call this method, Core Motion continually updates the gyroData property of CMMotionManager with the latest measurement of gyroscope activity. Then, you periodically sample this property. If you adopt this polling approach, set the update-interval property (gyroUpdateInterval) to the maximum interval at which Core Motion performs updates.

  • startGyroUpdatesToQueue:withHandler:—the push approach

    Before you call this method, assign an update interval to the gyroUpdateInterval property, create an instance of NSOperationQueue, and implement a block of type CMGyroHandler that handles the gyroscope updates. Then, call the startGyroUpdatesToQueue:withHandler: method on the motion-manager object, passing in the operation queue and the block. At the specified update interval, Core Motion passes the latest sample of gyroscope activity to the block, which executes as a task in the queue.

    Listing 4-5 demonstrates this approach.

Listing 4-5 is also extracted from the MotionGraphs sample code project, and is nearly identical to Listing 4-4. The app uses the push approach to retrieve gyroscope data so that it can plot the data onscreen.

Listing 4-5  Accessing gyroscope data in MotionGraphs

static const NSTimeInterval gyroMin = 0.01;
 
- (void)startUpdatesWithSliderValue:(int)sliderValue {
 
     // Determine the update interval
     NSTimeInterval delta = 0.005;
     NSTimeInterval updateInterval = gyroMin + delta * sliderValue;
 
     // Create a CMMotionManager
     CMMotionManager *mManager = [(APLAppDelegate *)[[UIApplication sharedApplication] delegate] sharedManager];
     APLGyroGraphViewController * __weak weakSelf = self;
 
     // Check whether the gyroscope is available
     if ([mManager isGyroAvailable] == YES) {
          // Assign the update interval to the motion manager
          [mManager setGyroUpdateInterval:updateInterval];
          [mManager startGyroUpdatesToQueue:[NSOperationQueue mainQueue] withHandler:^(CMGyroData *gyroData, NSError *error) {
               [weakSelf.graphView addX:gyroData.rotationRate.x y:gyroData.rotationRate.y z:gyroData.rotationRate.z];
               [weakSelf setLabelValueX:gyroData.rotationRate.x y:gyroData.rotationRate.y z:gyroData.rotationRate.z];
          }];
     }
     self.updateIntervalLabel.text = [NSString stringWithFormat:@"%f", updateInterval];
}
 
- (void)stopUpdates{
     CMMotionManager *mManager = [(APLAppDelegate *)[[UIApplication sharedApplication] delegate] sharedManager];
     if ([mManager isGyroActive] == YES) {
          [mManager stopGyroUpdates];
     }
}

Handling Processed Device Motion Data

If a device has both an accelerometer and a gyroscope, Core Motion offers a device-motion service that processes raw motion data from both sensors. Device motion uses sensor fusion algorithms to refine the raw data and generate information for a device’s attitude, its unbiased rotation rate, the direction of gravity on a device, and the user-generated acceleration. An instance of the CMDeviceMotion class encapsulates all of this data. Additionally, you do not need to filter the acceleration data because device-motion separates gravity and user acceleration.

You can access attitude data through a CMDeviceMotion object’s attitude property, which encapsulates a CMAttitude object. Each instance of the CMAttitude class encapsulates three mathematical representations of attitude:

  • a quaternion

  • a rotation matrix

  • the three Euler angles (roll, pitch, and yaw)

To start receiving and handling device-motion updates, create an instance of the CMMotionManager class and call one of the following two methods on it:

Listing 4-6 uses code from the pARk sample code project to demonstrate how to start and stop device motion updates. The startDeviceMotion method uses the pull approach to start device updates with a reference frame. See “Device Attitude and the Reference Frame” for more about device motion reference frames.

Listing 4-6  Starting and stopping device motion updates

- (void)startDeviceMotion {
     // Create a CMMotionManager
     motionManager = [[CMMotionManager alloc] init];
 
     // Tell CoreMotion to show the compass calibration HUD when required
     // to provide true north-referenced attitude
     motionManager.showsDeviceMovementDisplay = YES;
     motionManager.deviceMotionUpdateInterval = 1.0 / 60.0;
 
     // Attitude that is referenced to true north
     [motionManager startDeviceMotionUpdatesUsingReferenceFrame:CMAttitudeReferenceFrameXTrueNorthZVertical];
}
 
 
- (void)stopDeviceMotion {
     [motionManager stopDeviceMotionUpdates];
}

Device Attitude and the Reference Frame

A CMDeviceMotion object contains information about a device’s attitude, or orientation in space. Device attitude is always measured in relation to a reference frame. Core Motion establishes the reference frame when your app starts device-motion updates. Then, CMAttitude gives the rotation from that initial reference frame to the device's current reference frame.

In the Core Motion reference frame, the z-axis is always vertical, and the x- and y-axis are always orthogonal to gravity, which makes the gravity vector [0, 0, -1]. This is also known as the gravity reference. If you multiply the rotation matrix obtained from a CMAttitude object by the gravity reference, you get gravity in the device's frame. Or, mathematically:

../Art/rm_times_gravityret_2x.png

You can change the reference frame that CMAttitude uses. To do that, cache the attitude object that contains the reference frame and pass it as the argument to multiplyByInverseOfAttitude:. The attitude argument receiving the message changes so that it represents the change in attitude from the passed-in reference frame.

Most apps are interested in the change in device attitude. To see how this might be useful, consider a baseball game where the user rotates the device to swing. Normally, at the beginning of a pitch, the bat would be at some resting orientation. After that, the bat is rendered based on how the device's attitude changed from the start of a pitch. Listing 4-7 illustrates how you might do this.

Listing 4-7  Getting the change in attitude prior to rendering

-(void) startPitch {
    // referenceAttitude is a property
    self.referenceAttitude = self.motionManager.deviceMotion.attitude;
}
 
- (void)drawView {
    CMAttitude *currentAttitude = self.motionManager.deviceMotion.attitude;
    [currentAttitude multiplyByInverseOfAttitude: self.referenceAttitude];
    // Render bat using currentAttitude
    [self updateModelsWithAttitude:currentAttitude];
    [renderer render];
}

In this example, after the multiplyByInverseOfAttitude: method returns, currentAttitude represents the change in attitude from referenceAttitude to the most recently sampled CMAttitude instance.