Chapter    5

Motion Recipes

One of the more impressive features of iOS devices is the built-in motion sensor. With motion sensors, iOS developers can create absolutely amazing appsapplications we could only dream about back in early 2000. Nowadays we can just point our phones to the night sky and instantly learn the names of stars and constellations; we can play virtual marble labyrinth games that are so close to the real experience that it’s almost frightening. Motion sensors have truly enriched the field of app development.

Through the Core Motion framework you have easy access to the device’s accelerometer, gyroscope, and magnetometer. It is your job to employ these tools to enhance the user’s experience and create new cool features. The recipes in this chapter will help you get started.

All but the first recipe in this chapter require a physical device to test the functionality, because there currently is no way to simulate data from the Core Motion framework.

Recipe 5-1: Recognizing Shake Events

Before diving into the Core Motion framework let’s first deal with a related topic: the shaking of a device. A large number of applications utilize this functionality in a variety of ways, with results ranging from the shuffling of songs to the refreshing of information. While this implementation does not necessarily rely on the Core Motion framework, its key concept of being able to detect physical changes to your device makes it an important functionality to understand.

Intercepting Shake Events

Although you could use the Core Motion framework to identify shake events you’ll use the more convenient motionEnded:withEvent: message. When a user shakes the device, this message is being dispatched to the first responder of your application.

So, for example, you could set up your application’s main view to receive shake events using code as listed here.

@implementation ViewController

// . . .

- (BOOL) canBecomeFirstResponder
{
    return YES;
}

- (void) viewWillAppear: (BOOL)animated
{
    [self.view becomeFirstResponder];
    [super viewWillAppear:animated];
}

- (void) viewWillDisappear: (BOOL)animated
{
    [self.view resignFirstResponder];
    [super viewWillDisappear:animated];
}

- (void) motionEnded: (UIEventSubtype)motion withEvent: (UIEvent *)event
{
    if (event.subtype == UIEventSubtypeMotionShake)
    {
        // Device was shaken
    }
}


// . . .

@end

While the code in Listing 5-1 works for most situations, a slightly more sophisticated solution allows you to recognize shake events from all over your application. The next section shows you how to do it.

Subclassing the Window

If there are no receivers of the motionEnded:withEvent: message within the chain of responders, the message is sent to the application’s window object. What you want to do is to intercept motionEnded:withEvent: on the window object and implement your own notification scheme, letting any interested object in your application know when the device has been shaken.

The first thing you need to do is to subclass the application’s window object. To do that, create a new Objective-C class and name it MainWindow. Be sure to make it a subclass of UIWindow (see Figure 5-1).

9781430245995_Fig05-01.jpg

Figure 5-1.  Subclassing UIWindow by creating an Objective-C class

Next change the application’s setup code to use your custom window class. This is done with a small change in the app delegate’s didFinishLaunchingWithOptions: method. Depending on which template you used to create the application project, the surrounding code may be different but the change is the same. Here’s how it looks in a single-view application (you’ll also need to import the MainWindow.h file in your AppDelegate.h file or the following code won’t compile):

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[MainWindowalloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    // Override point for customization after application launch.
    self.viewController = [[ViewController alloc] initWithNibName:@"ViewController" bundle:nil];
    self.window.rootViewController = self.viewController;
    [self.window makeKeyAndVisible];
    return YES;
}

Now you have an architecture that allows you to intercept events sent to the main window. It’s time to implement your application-wide shake notifications.

Implementing Shake Notifications

In MainWindow.h, add the following code.

@implementation MainWindow

// . . .

- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
{
    if (event.type == UIEventTypeMotion && event.subtype == UIEventSubtypeMotionShake)
    {
        [[NSNotificationCenter defaultCenter] postNotificationName:@"NOTIFICATION_SHAKE"
                                              object:self];
    }
}


// . . .

@end

This code intercepts a shake event and uses the NSNotificationCenter class to post a notification. NSNotificationCenterimplements an Observer pattern for simple notifications reachable from any part of your application. The type of notification is identified by its name; this uses NOTIFICATION_SHAKE but you can pick any name you like.

An object interested in your shake notifications can register an action method with the notification center. As an example, let’s say you want to be notified about shakes in the application’s main view. You could then do something such as the code here:

@implementation ViewController

// . . .

- (void) viewWillAppear: (BOOL)animated
{
    [[NSNotificationCenter defaultCenter] addObserver:self
                                          selector:@selector(shakeDetected:)
                                          name:@"NOTIFICATION_SHAKE" object:nil];

    [super viewWillAppear:animated];
}

- (void) viewWillDisappear: (BOOL)animated
{
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    [super viewWillDisappear:animated];
}

-(void)shakeDetected:(NSNotification *)paramNotification
{
    NSLog(@"Shaken not stirred");

}

// . . .

@end

As you can see in the preceding code, if the view is removed from sight, you unregister the observer that listens to the shake notifications. This may or may not be what you want. If you want to keep on tracking shake events even though the view has disappeared, don’t leave the call to removeObserver: out.

Testing Shake Events

Now you’re finished and you can run and test your application.

Although Core Motion features require a real device to be tested, shake events can actually be tested within the simulator. Simply use the Shake Gesture item under the Hardware menu, as shown in Figure 5-2.

9781430245995_Fig05-02.jpg

Figure 5-2.  Shake events can be simulated

Figure 5-3 shows the test application after it has responded to a shake event.

9781430245995_Fig05-03.jpg

Figure 5-3.  Your test app writes to the output console upon shake events

Recipe 5-2: Accessing Raw Core Motion Data

Using this recipe, you’ll create a simple application that receives and displays the raw data from the accelerometer, the gyroscope, and the magnetometer sensors. You’ll need a real device that has these sensors (e.g., an iPhone 4) to test the app.

The Core Motion Sensors

In Core Motion, you can access three different pieces of hardware on a device, assuming that the device is new enough to be equipped with said hardware (see Table 5-1).

  • The accelerometer measures acceleration, caused by gravity or user acceleration, of the device. The information can provide insight into the current orientation, as well as the current general movement of the device.
  • The gyroscope measures rotation of the device along multiple axes.
  • The magnetometer provides data regarding the magnetic field passing through the device. This is normally the Earth’s magnetic field, but it may also be any other magnetic fields nearby. Remember to be careful when testing this feature; placing any kind of powerful magnet near your device could harm it.

Table 5-1 shows the availability of the sensors on various iOS devices.

Table 5-1. Sensor Support on Various Devices

Accelerometer Available on all iPhones, iPads, and iPods.
Gyroscope Available on iPhone 4, iPad2, iPod 4, and later.
Magnetometer Available on iPhone 3GS and later, as well as on all iPads.

For all three sensors, data comes in the form of a three-dimensional vector, with components X, Y, and Z. If you are holding your device facing you with the bottom facing the ground, the x-axis cuts horizontally through your device; the y-axis runs vertically from bottom up, and the z-axis runs through the center of the device toward you.

In the case of the gyroscope, the values are the rotation rate around these axes. To find out which direction results in positive rotation rate values you can use the right-hand rule. Imagine that you hold your open right hand in such a way that your thumb points toward the positive end of the axis. Then a positive rotation around the axis is the direction in which your fingers curve when you close your hand.

Rotation around the X, Y, and Z axes are called pitch, roll, and yaw, respectively. Figure 5-4 shows the axes and the positive directions of these rotations.

9781430245995_Fig05-04.jpg

Figure 5-4.  Directions and rotations as defined in iOS

Setting Up the Project

You will create a simple app that displays the current data from the three sensors. As you move the device around you can see how the movement affects their output.

Begin by creating a new single-view application and give it a suitable project name, for example “Raw Motion Data Test.” Now, because you’re going to use the Core Data framework you need to link CoreMotion.framework binary to your project.

Next, add labels to the main view and organize them so that the view resembles Figure 5-5. You will need 21 labels.

9781430245995_Fig05-05.jpg

Figure 5-5.  Main view interface

Your app is going to update the labels containing values (i.e., the ones with the text 0.0 in Figure 5-4). Therefore, create outlets for those nine labels so that you’ll be able to change their text at runtime.

Give the outlets the following names.

  • Accelerometer value labels: xAccLabel, yAccLabel, and zAccLabel.
  • Gyroscope value labels: xGyroLabel, yGyroLabel, and zGyroLabel.
  • Magnetometer value labels: xMagLabel, yMagLabel, and zMagLabel.

For instructions on how to create outlets for view components, see the corresponding recipes in Chapter 1.

When done, your view controller interface declaration should resemble this:

@interface ViewController : UIViewController

@property (strong, nonatomic) IBOutlet UILabel *xAccLabel;
@property (strong, nonatomic) IBOutlet UILabel *yAccLabel;
@property (strong, nonatomic) IBOutlet UILabel *zAccLabel;
@property (strong, nonatomic) IBOutlet UILabel *xGyroLabel;
@property (strong, nonatomic) IBOutlet UILabel *yGyroLabel;
@property (strong, nonatomic) IBOutlet UILabel *zGyroLabel;
@property (strong, nonatomic) IBOutlet UILabel *xMagLabel;
@property (strong, nonatomic) IBOutlet UILabel *yMagLabel;
@property (strong, nonatomic) IBOutlet UILabel *zMagLabel;

@end

Now you have the basic structure for the application. It’s time to dig out the data from the Core Motion framework.

Accessing Sensor Data

The Core Motion framework relies heavily on a single class called CMMotionManager. This class acts as a hub through which you access the motion sensors. You’ll set up a lazy initialization property to access a single instance of that class.

Make the following changes to the view controller’s header class. Note that the outlet properties have been removed for the sake of brevity. Don’t remove them in your code.

#import <UIKit/UIKit.h>
#import <CoreMotion/CoreMotion.h>

@interface ViewController : UIViewController
// . . .

@property (strong, nonatomic) CMMotionManager *motionManager;

@end

Now switch to ViewController.mand add the following code to the view controller’s implementation section. Again, code has been removed for brevity.

@implementation ViewController

// . . .

-(CMMotionManager *)motionManager
{
    // Lazy initialization
    if (_motionManager == nil)
    {
        _motionManager = [[CMMotionManager alloc] init];
    }
    return _motionManager;
}


// . . .
@end

Because you’ll be moving around the device to get different readings, you should stop the autorotation of the user interface. You do that by setting portrait as the only supported interface orientation for this application. You can do that in the Supported Interface Orientations section on the Project Editor’s Summary page (see Figure 5-6).

9781430245995_Fig05-06.jpg

Figure 5-6.  Setting supported interface orientations

What you want to do is to start receiving data from your sensors and update the respective labels with the information. To do that you need to perform the following steps:

  1. Check whether the sensor in question is available.
  2. Set an update interval.
  3. Start the updating and provide the piece of code that shall be invoked on each interval.

Translated into code this becomes, here for the accelerator, the following:

// Start accelerometer if available
if ([self.motionManager isAccelerometerAvailable])
{
    //Update twice per second
    [self.motionManager setAccelerometerUpdateInterval:1.0/2.0];
    [self.motionManager startAccelerometerUpdatesToQueue:[NSOperationQueue mainQueue]
                        withHandler:
        ^(CMAccelerometerData *data, NSError *error)
         {
             // New data arrived, update accelerometer labels
             self.xAccLabel.text = [NSString stringWithFormat:@"%f",
                                    data.acceleration.x];
             self.yAccLabel.text = [NSString stringWithFormat:@"%f",
                                    data.acceleration.y];
             self.zAccLabel.text = [NSString stringWithFormat:@"%f",
                                    data.acceleration.z];
         }
    ];
}

The startAcceleratorUpdatesToQueue:withHandler: method retains the code block (the so-called handler) and executes it as a task within the given operation queue repeatedly on the given interval. In our case, it will result in the accelerator labels being updated with the latest values from the accelerometer.

The other sensors have a similar programming interface. The following code shows methods that start and stop all three sensors at once. Add them to your project’s view controller.

- (void)startUpdates
{
    // Start accelerometer if available
    if ([self.motionManager isAccelerometerAvailable])
    {
        [self.motionManager setAccelerometerUpdateInterval:1.0/2.0];
        [self.motionManager startAccelerometerUpdatesToQueue:
                            [NSOperationQueue mainQueue]
                            withHandler:
         ^(CMAccelerometerData *data, NSError *error)
         {
             self.xAccLabel.text = [NSString stringWithFormat:@"%f",
                                    data.acceleration.x];
             self.yAccLabel.text = [NSString stringWithFormat:@"%f",
                                    data.acceleration.y];
             self.zAccLabel.text = [NSString stringWithFormat:@"%f",
                                    data.acceleration.z];
         }];
    }
    // Start gyroscope if available
    if ([self.motionManager isGyroAvailable])
    {
        [self.motionManager setGyroUpdateInterval:1.0/2.0];
        [self.motionManager startGyroUpdatesToQueue:
                            [NSOperationQueue mainQueue]
                            withHandler:
         ^(CMGyroData *data, NSError *error)
         {
             self.xGyroLabel.text = [NSString stringWithFormat:@"%f",
                                     data.rotationRate.x];
             self.yGyroLabel.text = [NSString stringWithFormat:@"%f",
                                     data.rotationRate.y];
             self.zGyroLabel.text = [NSString stringWithFormat:@"%f",
                                     data.rotationRate.z];
         }];
    }
    // Start magnetometer if available
    if ([self.motionManager isMagnetometerAvailable])
    {
        [self.motionManager setMagnetometerUpdateInterval:1.0/2.0];
        [self.motionManager startMagnetometerUpdatesToQueue:[NSOperationQueue mainQueue]
                            withHandler:
         ^(CMMagnetometerData *data, NSError *error)
         {
             self.xMagLabel.text = [NSString stringWithFormat:@"%f",
                                    data.magneticField.x];
             self.yMagLabel.text = [NSString stringWithFormat:@"%f",
                                    data.magneticField.y];
             self.zMagLabel.text = [NSString stringWithFormat:@"%f",
                                    data.magneticField.z];
         }];
    }
}

-(void)stopUpdates
{
    if ([self.motionManager isAccelerometerAvailable] &&
        [self.motionManager isAccelerometerActive])
    {
        [self.motionManager stopAccelerometerUpdates];
    }
    if ([self.motionManager isGyroAvailable] &&
        [self.motionManager isGyroActive])
    {
        [self.motionManager stopGyroUpdates];
    }
    if ([self.motionManager isMagnetometerAvailable] &&
        [self.motionManager isMagnetometerActive])
    {
        [self.motionManager stopMagnetometerUpdates];
    }
}

Now what’s left is to find suitable places to invoke startUpdates and stopUpdates. This time start the updates when the app becomes active and stop them when resigning from active state (i.e., when the app enters the background state). This can be done from your app delegate. For that to work you need to make the startUpdates and stopUpdates methods public. So add the following code to ViewController.h:

#import <UIKit/UIKit.h>
#import <CoreMotion/CoreMotion.h>

@interface ViewController : UIViewController

// . . . Outlet properties removed for brevity
@property (nonatomic, strong) CMMotionManager *motionManager;

- (void)startUpdates;
- (void)stopUpdates;


@end

Now add the following code to your application delegate (in AppDelegate.m).

- (void)applicationWillResignActive:(UIApplication *)application
{
    [self.viewController stopUpdates];
}

- (void)applicationDidBecomeActive:(UIApplication *)application
{
    [self.viewController startUpdates];
}

Your app is now finished and ready to run. Remember, the simulator has no support for any of the three sensors so nothing interesting will happen during simulation. You need a real device to test this app. Figure 5-7 shows a screenshot of the app in action.

9781430245995_Fig05-07.jpg

Figure 5-7.  An app displaying raw Core Motion data

Pushing or Pulling

There are two ways to access updated data from the sensors. In this recipe you used ”pushing.” What this means is that the motion manager will invoke your code on the given intervals and provide it with the new values. This system is implemented in the start<Sensor>UpdatesToQueue:withHandler methods, where you provide the code in the form of a block.

The other strategy is “pulling,” and it’s the preferred method if your app has a render loop from which you can query the values yourself on a regular basis. This can be a little more efficient and is generally better suited for game apps.

Pulling is implemented in the start<Sensor>Updates method. It keeps the properties accelerometerData, gyroData, and magnetometerData on the motion manager updated with the most recent value. Your app can then, at its own convenience, retrieve the values from those properties, as the following code shows.

// Start updates in pull mode
[self.motionManager startAccelerometerUpdates];

// Pull the values from somewhere within an update loop
self.xMagLabel.text = [NSString stringWithFormat:@"%f",
                       self.motionManager.magnetometerData.magneticField.x];
self.yMagLabel.text = [NSString stringWithFormat:@"%f",
                       self.motionManager.magnetometerData.magneticField.y];
self.zMagLabel.text = [NSString stringWithFormat:@"%f",
                       self.motionManager.magnetometerData.magneticField.z];

Selecting an Update Interval

You can set the update interval to as little as one update each ten milliseconds (1/100). However, you should strive to the highest possible value that will work for your application because that will improve battery time. Table 5-2 provides a general guideline for update intervals.

Table 5-2. Guideline Values for Update Intervals

Update Interval Usage Example
10 ms (1/100) For detecting high-frequency motion.
20 ms (1/50) Suitable for games that uses real-time user input.
100 ms (1/10) Suitable for determining the current orientation of the device.

The Nature of Raw Motion Data

When running the app, you may have discovered how incomprehensible the data from the sensors are. This is because the data from the sensors are biasedthat is, they are affected by more than one force. The accelerometer, for instance, is affected by Earth’s gravity as well as the movement from the user’s hand. The magnetometer senses not only the magnetic field of the Earth but also all other magnetic fields in your vicinity.

The biased nature of the raw data makes the data difficult to interpret. You need tricks like high and low pass filters to isolate the various components. Fortunately, Core Motion comes with a way to access unbiased data from the sensors which makes it easy to figure out the devices’ real orientation and motion. Recipe 5-3 teaches you how to use this convenient feature.

Recipe 5-3: Accessing Device Motion Data

The previous recipe showed you how to access raw motion data from the three sensors. While easy to access, the raw biased data are not easy to use. They require various filtering techniques to isolate the different forces that affect the sensors to make real use of the data. The good news is that Apple has done the dirty work for you, ready to be utilized via the deviceMotion property of the motion manager. This recipe shows you how.

The Device Motion Class

Just like the accelerometer, gyroscope, and magnetometer from the previous recipe, you can access CMDeviceMotion by starting and stopping updates using very similar methods: startDeviceMotionUpdates and startDeviceMotionUpdatesToQueue:withHandler:. However, you also have two extra methods that allow you to specify a “reference frame,” startDeviceMotionUpdatesUsingReferenceFrame: and startDeviceMotionUpdatesUsingReferenceFrame:toQueue:WithHandler:. The reference frame is discussed shortly.

When retrieving data using an instance of CMDeviceMotion (through the deviceMotion property in your CMMotionManager), you can access six different properties.

  • The attitude property is an instance of the CMAttitude class. It gives you a detailed insight into the device’s orientation at a given time as compared to a reference frame. Through this class you can access properties such as roll, pitch, and yaw. These values are measured in radians and allow you an accurate measurement of your device’s orientation.
  • As shown previously in Figure 5-4, roll specifies the device’s position of rotation around the y-axis, pitch the position of rotation around the x-axis, and yaw around the z-axis.
  • The rotationRate property is just like the one you saw in the previous recipe, except that it gives a more accurate reading. It does this by reducing device bias that causes a still device to have nonzero rotation values.
  • The gravity property represents the acceleration caused solely by gravity on the device.
  • The userAcceleration represents the physical acceleration imparted on a device by the user outside gravitational acceleration.
  • The magneticFieldvalue is similar to the one you saw in Recipe 5-2; however, it removes any device bias, resulting in more accurate readings.

Note  If you are unfamiliar with them, radians are a different way of measuring rotation from the more commonly used degrees. They are based around the value pi (π). A radian value of pi (roughly 3.14) is equivalent to a 180-degree rotation, so any radian value can be converted to degrees by dividing by pi, and then multiplying by 180 like so: d = r * 180 / π.

Setting Up the Application

You’ll create an application similar to the one you built in Recipe 5-2. So go ahead and create a new single-view application project and link in the Core Motion framework.

Caution  Failing to link the Core Motion framework results in a linker error, such as the one that follows, when you try to build your application later on.

Undefined symbols for architecture armv7:

    "_OBJC_CLASS_$_CMMotionManager", referenced from:

             objc-class-ref in ViewController.o

Now create a user interface like the one in Figure 5-8. You’ll need 35 label objects, 15 of which are displaying values. Because you will update those labels at runtime you’ll need to create outlets for them. Use the following names for the outlets:

  • Attitude value labels: rollLabel, pitchLabel, and yawLabel.
  • Rotation rate value labels: xRotLabel, yRotLabel, and zRotLabel.
  • Gravity value labels: xGravLabel, yGravLabel, and zGravLabel.
  • User acceleration labels: xAccLabel, yAccLabel, and zAccLabel.
  • Magnetic field labels: xMagLabel, yMagLabel, and zMagLabel.

9781430245995_Fig05-08.jpg

Figure 5-8.  User interface of the device motion app

Now, as you did in Recipe 5-2, add a motion manager property to the view controller.

#import <UIKit/UIKit.h>
#import <CoreMotion/CoreMotion.h>

@interface ViewController : UIViewController

// . . .

@property (strong, nonatimic) CMMotionManager *motionManager;

@end

The property should have the same lazy initialization implementation as before:

@implementation ViewController

// . . .

-(CMMotionManager *)motionManager
{
    // Lazy initialization
    if (_motionManager == nil)
    {
        _motionManager = [[CMMotionManager alloc] init];
    }
    return _motionManager;

}

// . . .

@end

Also, as in Recipe 5-2, your app should support only portrait orientation.

Accessing Device Motion Data

Starting and stopping updates and retrieving the data from the device motion property follow the same pattern that you used for accessing the three sensors’ raw data. The difference is that you only need one start statement, which allows you to reach all the data at once.

So, add the following methods to your view controller. Be sure to add their declarations to the header file as well because you’ll invoke them from the application delegate later.

- (void)startUpdates
{
    // Start device motion updates
    if ([self.motionManager isDeviceMotionAvailable])
    {
         //Update twice per second
        [self.motionManager setDeviceMotionUpdateInterval:1.0/2.0];
        [self.motionManager startDeviceMotionUpdatesToQueue:[NSOperationQueue mainQueue]
                                                 withHandler:
         ^(CMDeviceMotion *deviceMotion, NSError *error)
         {
             // Update attitude labels
             self.rollLabel.text =  [NSString stringWithFormat:@"%f",
                                     deviceMotion.attitude.roll];
             self.pitchLabel.text = [NSString stringWithFormat:@"%f",
                                     deviceMotion.attitude.pitch];
             self.yawLabel.text =   [NSString stringWithFormat:@"%f",
                                     deviceMotion.attitude.yaw];
             // Update rotation rate labels
             self.xRotLabel.text = [NSString stringWithFormat:@"%f",
                                    deviceMotion.rotationRate.x];
             self.yRotLabel.text = [NSString stringWithFormat:@"%f",
                                    deviceMotion.rotationRate.y];
             self.zRotLabel.text = [NSString stringWithFormat:@"%f",
                                    deviceMotion.rotationRate.z];

             // Update user acceleration labels
             self.xGravLabel.text = [NSString stringWithFormat:@"%f",
                                     deviceMotion.gravity.x];
             self.yGravLabel.text = [NSString stringWithFormat:@"%f",
                                     deviceMotion.gravity.y];
             self.zGravLabel.text = [NSString stringWithFormat:@"%f",
                                     deviceMotion.gravity.z];

             // Update user acceleration labels
             self.xAccLabel.text = [NSString stringWithFormat:@"%f",
                                    deviceMotion.userAcceleration.x];
             self.yAccLabel.text = [NSString stringWithFormat:@"%f",
                                    deviceMotion.userAcceleration.y];
             self.zAccLabel.text = [NSString stringWithFormat:@"%f",
                                    deviceMotion.userAcceleration.z];

             // Update magnetic field labels
             self.xMagLabel.text = [NSString stringWithFormat:@"%f",
                                    deviceMotion.magneticField.field.x];
             self.yMagLabel.text = [NSString stringWithFormat:@"%f",
                                    deviceMotion.magneticField.field.y];
             self.zMagLabel.text = [NSString stringWithFormat:@"%f",
                                    deviceMotion.magneticField.field.z];
         }];
    }
}

-(void)stopUpdates
{
    if ([self.motionManager isDeviceMotionAvailable] &&
        [self.motionManager isDeviceMotionActive])
    {
        [self.motionManager stopDeviceMotionUpdates];
    }
}

Finally, invoke the start and stop updates methods from the app delegate’s applicationWillResignActive: and applicationDidBecomeActive: methods, respectively.

- (void)applicationWillResignActive:(UIApplication *)application
{
    [self.viewController stopUpdates];
}

- (void)applicationDidBecomeActive:(UIApplication *)application
{
    [self.viewController startUpdates];
}

So now if you run this application, you will probably notice that most of your values are more stable than those from the raw sensor data of Recipe 5-2. You may also see all zeros for your magnetometer readings. Move your device in a figure-eight motion to calibrate your magnetometer until these values start updating.

Setting a Reference Frame

Though not required, you can specify a reference frame for your attitude data. This is done by using the startDeviceMotionUpdatesUsingReferenceFram:toQueue:withHandler: method.

One of the following is a possible value for the reference frame parameter:

  • CMAttitudeReferenceFrameXArbitraryZVertical, which specifies a reference frame with the z-axis along the vertical and the x-axis along any arbitrary direction; simply put, the device is flat and face-up.
  • CMAttitudeReferenceFrameXArbitraryCorrectedZVertical, which is the same as the previous value except the magnetometer is used to provide better accuracy. This option increases CPU (central processing unit) usage and requires the magnetometer to be both available and calibrated.
  • CMAttitudeReferenceFrameXMagneticNorthZVertical, which reference a frame that has the z-axis vertical as before, but with the x-axis directed toward “magnetic north.” This option requires the magnetometer to be available and calibrated, which means you will probably have to wave your device around a bit before you can get any readings in your application.
  • CMAttitudeReferenceFrameXTrueNorthZVertical, which is just like the previous, but the x-axis is directed toward “true north” rather than “magnetic north.” The location of the device must be available for the device to be able to calculate the difference between the two.

Note  There’s a difference between “magnetic north” and “true north.” Magnetic north is the magnetic north pole of the Earth, which is where any compass will point. This point, however, is not constant due to changes in the Earth’s core and is moving more than 30 miles per year. True north refers to the direction toward the actual north pole of the Earth, which stays constant.

You will choose the third option, CMAttitudeReferenceFrameXMagneticNorthZVertical, for your application. Change your call to the startDeviceMotionUpdatesToQueue:withHandler: method to the following:

[self.motionManager startDeviceMotionUpdatesUsingReferenceFrame:
                    CMAttitudeReferenceFrameXMagneticNorthZVertical
                    toQueue:[NSOperationQueue mainQueue]
                    withHandler:
  ^(CMDeviceMotion *deviceMotion, NSError *error)
  {
    // . . . Update value labels here
  }];

Now, when you run your application, you may start off seeing “0.0” for all your values. If that’s the case, you can move your device around in a figure-eight motion to get your magnetometer calibrated; the values should start updating soon enough.

You should notice now that if you lay your device on a flat surface and then turn the device around the z-axis, at the moment that your x-axis is aligned with the Earth’s magnetic field, your yaw value should get close to zero, as in Figure 5-9.

9781430245995_Fig05-09.jpg

Figure 5-9.  Your application receiving calibrated device information

Recipe 5-4: Moving a Label with Gravity

Recipes 5-2 and 5-3 showed you how to access the various Core Motion data. It’s time to put that knowledge into use and make it a little more interesting. Using this recipe you’ll create an application with a single label that you’ll be able to move around by tilting your device.

Setting Up the Application

You’ll use the same basic architecture that you built in the previous two recipes. So once again start by creating a new single-view application and linking it to CoreMotion.framework. Then add the following declarations, which should be familiar by now, to your view control’s header file:

#import <UIKit/UIKit.h>
#import <CoreMotion/CoreMotion.h>

@interface ViewController : UIViewController

@property (strong, nonatomic) CMMotionManager *motionManager;

- (void)startUpdates;

- (void)stopUpdates;

@end

Now switch toViewController.m and implement the property using lazy initialization; add stubs for the startUpdate and stopUpdate methods. See the following code:

@implementation ViewController
@synthesize motionManager = _motionManager;

- (CMMotionManager *)motionManager
{
    // Lazy initialization
    if (_motionManager == nil)
    {
        _motionManger = [[CMMotionManager alloc] init];
    }
    return _motionManager;
}

- (void)startUpdates
{
}
- (void)stopUpdates
{
}

// . . .

@end

As in the previous two recipes, your app should only support the portrait interface orientation, so make sure you make that change in the project settings.

Next, go to AppDelegate.mand add the code to start and stop the updates when the application goes in and out of the active state.

@implementation AppDelegate

// . . .

- (void)applicationWillResignActive:(UIApplication *)application
{
    [self.viewController stopUpdates];
}

// . . .

- (void)applicationDidBecomeActive:(UIApplication *)application
{
    [self.viewController startUpdates];
}

// . . .

@end

Finally, in the main view, add a label and create an outlet for it so that you’ll be able to change its position at runtime. Name the label’s outlet myLabel. Your user interface should look something like the one in Figure 5-10.

9781430245995_Fig05-10.jpg

Figure 5-10.  The user interface with the label you will move using gravity

Your app is now ready for the next step, implementing gravity-caused movement of the label.

Moving the Label with Gravity

You’re going to use a very simple algorithm for the initial version of your label moving feature. Later you’ll spice it up with a little acceleration, but for now you will settle for a linear movement that is proportional to the angle in which you tilt the device.

As you can see, you have increased the update frequency to once per 20 milliseconds (1/50). This enhances the feeling and responsiveness of the app. The next thing to notice is that you use the CMAttitudeReferenceFrameXArbitraryCorrectedZVertical reference frame. This gives you a z-axis that’s aligned with Earth’s gravity force, which is what you want.

- (void)startUpdates
{
    if ([self.motionManager isDeviceMotionAvailable] &&
        ![self.motionManager isDeviceMotionActive])
    {
        [self.motionManager setDeviceMotionUpdateInterval:1.0/50.0];
        [self.motionManager startDeviceMotionUpdatesUsingReferenceFrame:
                            CMAttitudeReferenceFrameXArbitraryCorrectedZVertical
                            toQueue:[NSOperationQueue mainQueue]
                            withHandler:
         ^(CMDeviceMotion *motion, NSError *error)
         {
             CGRect labelRect = self.myLabel.frame;
             double scale = 5.0;
             // Calculate movement on the x-axis
             double dx = motion.gravity.x * scale;
             labelRect.origin.x += dx;
             // Don't move outside the view's x bounds
             if (labelRect.origin.x < 0)
             {
                 labelRect.origin.x = 0;
             }
             else if (labelRect.origin.x + labelRect.size.width >
                      self.view.bounds.size.width)
             {
                 labelRect.origin.x =
                     self.view.bounds.size.width – labelRect.size.width;
             }
             // Calculate movement on the y-axis
             double dy = motion.gravity.y * scale;
             labelRect.origin.y -= dy;
             // Don't move outside the view's y bounds
             if (labelRect.origin.y < 0)
             {
                 labelRect.origin.y = 0;
             }
             else if (labelRect.origin.y + labelRect.size.height >
                      self.view.bounds.size.height)
             {
                 labelRect.origin.y =
                     self.view.bounds.size.height - labelRect.size.height;
             }

             [self.myLabel setFrame:labelRect];
         }];
    }

}

- (void)stopUpdates
{
    if ([self.motionManager isDeviceMotionAvailable] &&
        [self.motionManager isDeviceMotionActive])
    {
        [self.motionManager stopDeviceMotionUpdates];
    }

}

The algorithm in the preceding code is the simplest possible. Use the deviceMotion.gravity property as a velocity value (although it’s actually an acceleration) and calculate the delta movement from it. Because each value only reaches between −1.0 and 1.0 you use a scaling factor to adjust the general speed of the movement.

If you run the app now you should see the label moving in the direction you tilt your device; the bigger the tilt, the faster the movement. But the movement feels a bit unnatural. This is because it lacks an important component: acceleration. Acceleration is the topic of the next section.

Adding Acceleration

Now you’ll adjust the startUpdates method to implement a simple acceleration algorithm. Change your code according to this listing:

- (void)startUpdates
{
    if ([self.motionManager isDeviceMotionAvailable] &&
        ![self.motionManager isDeviceMotionActive])
    {
        __block double accumulatedDx = 0;
        __block double accumulatedDy = 0;


        [self.motionManager setDeviceMotionUpdateInterval:1.0/50.0];
        [self.motionManager startDeviceMotionUpdatesUsingReferenceFrame:
                            CMAttitudeReferenceFrameXArbitraryCorrectedZVertical
                            toQueue:[NSOperationQueue mainQueue]
                            withHandler:
         ^(CMDeviceMotion *motion, NSError *error)
         {
             CGRect labelRect = self.myLabel.frame;
             double scale = 1.5;
             double dx = motion.gravity.x * scale;
             accumulatedDx += dx;
             labelRect.origin.x += accumulatedDx;
             if (labelRect.origin.x < 0)
             {
                 labelRect.origin.x = 0;
                 accumulatedDx = 0;
             }
             else if (labelRect.origin.x + labelRect.size.width >
                      self.view.bounds.size.width)
             {
                 labelRect.origin.x =
                     self.view.bounds.size.width - labelRect.size.width;
                 accumulatedDx = 0;
             }
             double dy = motion.gravity.y * scale;
             accumulatedDy += dy;
             labelRect.origin.y -=accumulatedDy;
             if (labelRect.origin.y < 0)
             {
                 labelRect.origin.y = 0;
                 accumulatedDy = 0;
             }
             else if (labelRect.origin.y + labelRect.size.height > self.view.bounds.size.height)
             {
                 labelRect.origin.y = self.view.bounds.size.height - labelRect.size.height;
                 accumulatedDy = 0;
             }
             [self.myLabel setFrame:labelRect];
         }];
    }
}

Note  You may be wondering what the __block declarations in front of the accumulatedDx and accumulatedDy variables are. They are making the variables accessible from within a code block. What is more, they stay accessible even though the surrounding method has gone out of scope. This provides a clean and easy way for blocks to share variables, avoiding the need to create properties or global variables for local needs.

Notable, also, is that you have decreased the scaling factor. Now it’s not a problem if the label moves slowly at first; it’ll pick up pace soon enough thanks to the acceleration. You can play around with different values and find the one that you like best.

Last, but important, if the label reaches a border you reset the speed. Otherwise it’ll keep on accumulating speed (in the accumulatedXX variables) even though the movement has stopped, making it less responsive when you tilt the device in the opposite direction again.

Summary

This chapter discusses specific detail about accessing the multiple different values and information that the Core Motion framework has to offer. You went from raw data to more calibrated, functional values that you could translate into a mildly useful (if not slightly entertaining) application. Core Motion, however, is not a framework that can simply be an entire application in itself. You can use it to acquire values about your device, but you must then have the creativity to put them to use. From a simple application to measure the rotation speed of a person flipping to incorporating the magnetometer into an augmented-reality application, Core Motion provides a basic framework for accessing information, which can then translate into some of the most powerful pieces of software in iOS.

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

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