Chapter 12

Core Motion Recipes

The last two chapters spent an incredible amount of time on dealing with information stored and persisted inside a device’s memory. Now, we will deal with an entirely opposite topic: data from the outside world—not in the sense of data given by the user, but instead, data collected by the device about the universe in which it exists at any given second. By retrieving information about the outside world, a developer can specifically build applications focused on enhancing the user’s experience based on his or herphysical situation. This could be anything from simply detecting the orientation of the device to incorporating rotation into a racing game’s steering system, to using an accelerometer to measure the acceleration of a roller coaster. Through the use of the Core Motion framework, we are able to accesswith great ease a variety of hardware built into our iOS device in order to acquire such unique information, from magnetic fields, to accelerations, both by gravity and otherwise, to rotation rates, about the specific situation of our device.

For all but the first recipe in this chapter, you will need a physical device with which to test functionality, since youwill be dealing with information generated by the physical presence of a device.

Recipe 12–1: Registering Shake Events

Before we dive into the Core Motion framework, we can 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.

You will create a very simple application to detect and log the shaking of the device. From there, you will start to build an app that can read and display motion information.

Start off by creating a new project called “Measurements”, which you will use throughout this chapter. For your simple application, you will use the Single View Application template, as in Figure 12–1, so that you can be saved some work on putting your application delegate together.

Once you select the template, enter the project name, make sure that your class prefix is set to “Main” and that only the box marked “Use Automatic Reference Counting” is checked, and then click through to create your project.

Image

Figure 12–1. Configuring your UIWindow subclass

The first task involved in configuring the shaking functionality is that you need to instruct your application to post a notification any time the device is shaken (not stirred). To do this, you will need to subclass the UIWindow class. Create a new file using the “Objective-C class” template under the iOS Cocoa Touch section, as in Figure 12–2.

Image

Figure 12–2. Subclassing UIWindow by making an Objective-C class

Go ahead and name the class “MainWindow”, and make sure that you enter “UIWindow” in the “Subclass of” field, just as in Figure 12–3.

Image

Figure 12–3. Configuring your UIWindow subclass

Click through to create this subclass, and then switch over to its implementation file, called MainWindow.m.

In this class, you will be overriding the -motionEnded:withEvent: method in order to properly handle the shaking of your device. Any time the device is shaken, this method will automatically be called. You will write it to post a notification, which you can then track in other classes.

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

If you are not quite familiar with the concept of the NSNotificationCenter and posting notifications, you will quickly see that they are quite simple. Any given class can post notifications to the NSNotificationCenter. Any class that is “observing” the center for any notifications with the same “Name” will be notified, and can thus act accordingly. This is a fantastic way to move information between classes based on real-time events, and often deal with outside changes in userinput, such as adjustments to a device’s volume or using remote controls with a music application.

Next you need to specify that your new subclass of UIWindow is the one that your application should use. If you switch over to your application delegate, you will notice it already has a UIWindow property with which it displays all of your views.

First, add an import statement to your app delegate header file to include the MainWindow class.

#import "MainWindow.h"

Now in the app delegate implementation file, modify your -application:DidFinishLaunchingWithOptions: method to read like so:

- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
//////////The only changed line!
self.window = [[MainWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
//////////Change only the previous line.

self.viewController = [[MainViewController alloc] initWithNibName:@"MainViewController" bundle:nil];
self.window.rootViewController = self.viewController;
    [self.window makeKeyAndVisible];
return YES;
}

By modifying the class with which you allocated and initialized your window property from “UIWindow” to “MainWindow”, you threw a nice bit of polymorphism into your code, in that the window property is of type UIWindow, but specifically behaves like a MainWindow. Since MainWindow is a subclass of UIWindow, this won’t give you any problems, and saves you from having to change the property type as well.

Now, you can switch over to your main view controller, where you need to register for your “NOTIFICATION_SHAKE” notifications by modifying your -viewDidLoad method.

- (void)viewDidLoad
{
    [super viewDidLoad];

    [[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(shakeDetected:) name:@"NOTIFICATION_SHAKE" object:nil];

}

Since you passed the shakeDetected: selector as the designated action for these notifications in the previous method, you need to implement this in order to avoid any runtime exceptions. For now, your implementation will be very simple, but you will add onto it later.

-(void)shakeDetected:(NSNotification *)paramNotification
{
NSLog(@"Just be careful not to drop it!");
}

While it may seem a little odd, you do not actually need a physical device to test out this functionality. The iOS simulator includes a function to simulate a shaking motion on the device, so that you can see if your application responds correctly.

When viewing your simulator, navigate in the Hardware menu to the “Shake Gesture” (Ctrl+ImageZ) selection, as shown in Figure 12–4.

Image

Figure 12–4. Simulating a shake gesture

You Won't see any kind of shaking animation in the simulator, but it should respond appropriately to make your simple log. Figure 12–5 demonstrates the output of your application after “shaking” the simulator a couple of times.

Image

Figure 12–5. Resulting output from “shaking”your simulator

If you connect your iOS device to Xcode and run through that, you’ll get the same result once you give your device a bit of a shake!

Recipe 12–2: Accessing Raw Core Motion Data

Now that your application is set up with some basic shaking detection, you can start to build the Core Motion framework in, which will allow you access to accelerometer, gyroscope, and magnetometer information.

First, select your project, and then navigate to the Build Phases tab. Under Link Binary With Libraries, click the + button, and select the item labeled “CoreMotion.framework”, as in Figure 12–6.

Image

Figure 12–6. Adding the CoreMotion framework

Now back in your main view controller’s header file, add the following import statement so that you can access the Core Motion API.

#import <CoreMotion/CoreMotion.h>

The Core Motion framework relies extremely heavily on a single class called CMMotionManager. This class acts as a hub through which you access all the hardware that any given device is able to access. You will create an instance of this class in your view controller as a property in order to constantly keep track of it.

@property (nonatomic, strong) CMMotionManager *motionManager;

In your view controller, make sure to synthesize this property, and then set it to nil in your -viewDidUnload, as is standard.

Unfortunately, simply synthesizing a CMMotionManager object is not enough to ensure its initialization (like an NSArray), so you will create a modified getter method to make up for this. Luckily, there’s no complicated configuration needed to do this.

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

Next up, you’ll start by setting up your user interface. You will start off by measuring the output of three different instruments: the accelerometer, the gyroscope, and the magnetometer. You will go more into detail on the purposes of these instruments soon, but for now you must know that each of these will provide three different values for you to view. As such, you will set up your view as shown in Figure 12–7.

Image

Figure 12–7. Your view controller’s XIB setup

Make sure to connect the required UILabels to your view controller as properties so that you can change their text! You can leave all the device name labels as well as the “X”, “Y”, and “Z” labels alone, so you will connect only the UILabels thatwill show values (they read as “0.0” right now), as well as the bottom Status label. You will use the following property names in your code, organized by line:

  • xAccLabel, yAccLabel, zAccLabel
  • xGyroLabel, yGyroLabel, zGyroLabel
  • xMagLabel, yMagLabel, zMagLabel
  • statusLabel

You will also define a method in your header file to toggle whether you are currently retrieving hardware updates, so add the following method declaration.

-(void)toggleUpdates;

When you’re done, your user interface should be fully set up for your initial application, and your view controller’s header file, in its entirety, should look like so:

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

@interface MainViewController : UIViewController
@property (nonatomic, strong) CMMotionManager *motionManager;
@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;

@property (strong, nonatomic) IBOutlet UILabel *statusLabel;

-(void)toggleUpdates;
@end

Core Motion in Detail

In Core Motion, you are able to access three different pieces of hardware on a device, assuming that the device is new enough to be equipped with said hardware.

  1. Accelerometer: This piece of hardware measures acceleration (caused by gravity or user acceleration) of a device in order to provide insight into the current orientation of the device, as well as any accelerations of the device by the user in a specific direction. This is particularly useful for detecting whether the device is at an angle (and possibly rotating a view as a result).
    Data from the accelerometer is presented in the form of a CMAccelerometerData object. This object has just one property, acceleration, which has x, y, and z values, representing the orientation or acceleration along any given axis.
  2. Gyroscope: The gyroscope measures rotation rate of the device along multiple axes.
    Information from the gyroscope is enclosed in an instance of CMGyroData. This class has the property rotationRate, with x, y, and z values, which represent the rotation rate around a specific axis
  3. Magnetometer: This device provides data regarding the magnetic field passing through the device, usually based on the Earth’s magnetic field, as well as any other magnetic fields nearby. Remember to be careful when testing this feature, as placing any kind of powerful magnet near your device could possibly cause harm to it.
    When receiving information from the magnetometer, you access the CMMagnetometerData class, which has a single property, magneticField, which has x, y, and z values to represent the magnetic field through the device along each axis.

For all three devices, all axes are the same. If you are holding your device facing you with the bottom facing the ground, the xaxis cuts horizontally through your device, the yaxis runs from top to bottom, and the zaxis runs through the center of the device toward you.

You will start off by implementing your view controller to display the raw data for each of these pieces of hardware.

Whenever you are dealing with any internal hardware, it is the developer’s responsibility to include some way of confirming that the hardware being accessed is actually available on any given device running the application. For this reason, any time you access the Core Motion hardware, you will always make use of the following methods:

  • -isAccelerometerAvailable
  • -isGyroAvailable
  • -isMagnetometerAvailable

All three of these methods simply return a BOOL value, with YES indicating that the hardware is indeed accessible.

You will also make use of three other methods that check if any given hardware is currently already active to correctly toggle your updates.

  • -isAccelerometerActive
  • -isGyroActive
  • -isMagnetometerActive

Whenever you want to retrieve updates from a specific piece of hardware, you must set an “update interval,” to specify how often you will receive updates, as shown in the following example:

[self.motionManager setAccelerometerUpdateInterval:1.0/2.0]; //Update twice per second

There are two different methods by which you can receive updates from a particular hardware component. First, you can simply tell the CMMotionManager to start receiving updates from a device using -startAccelerometerUpdates, -startGyroUpdates, and -startMagnetometerUpdates, and then query the info using the accelerometerData, gyroData, and magnetometerData properties. This way is useful if you are not planning to make use of your updates every single time they refresh.

The second method focuses more for the user who wants to always utilize the newest data as it becomes available, as you are implementing here. You can make use of three different methods that not only start updates, but also specify a handler block of code to be performed upon updating, as well as a queue in which to perform the handler. These are listed as follows:

  • -startAccelerometerUpdatesToQueue:withHandler:
  • -startGyroUpdatesToQueue:withHandler:
  • -startMagnetometerUpdatesToQueue:withHandler:

For your simple uses, you will use the main queue to perform your handlers.

Combining all these methods discussed, you can build a simple section of code that, if run, will start updating with data from the accelerometer. This code will be used to build the method -toggleUpdates

if ([self.motionManager isAccelerometerAvailable] && ![self.motionManager isAccelerometerActive])
    {
        [self.motionManager setAccelerometerUpdateInterval:1.0/2.0]; //Update twice per second
        [self.motionManager startAccelerometerUpdatesToQueue:[NSOperationQueue mainQueue] withHandler:^(CMAccelerometerData *accelerometerData, NSError *error)
         {
self.xAccLabel.text = [NSString stringWithFormat:@"%f", accelerometerData.acceleration.x];
self.yAccLabel.text = [NSString stringWithFormat:@"%f", accelerometerData.acceleration.y];
self.zAccLabel.text = [NSString stringWithFormat:@"%f", accelerometerData.acceleration.z];
         }];
    }

As discussed earlier, you first check to make sure the hardware is both available and inactive. Next you set your update interval, and then start asking for updates. The simple handler you have specified will update your view’s information to include the raw data that you have received.

Since you are in fact building a method to toggle these updates on and off, you can add a following condition to turn off your updates.

else if ([self.motionManager isAccelerometerActive])
    {
        [self.motionManager stopAccelerometerUpdates];
self.xAccLabel.text = @"...";
self.yAccLabel.text = @"...";
self.zAccLabel.text = @"...";
    }

The other two instruments operate on the exact same principles. Overall, your -toggleUpdates method will be written like so:

-(void)toggleUpdates
{
if ([self.motionManager isAccelerometerAvailable] && ![self.motionManagerisAccelerometerActive])
    {
        [self.motionManager setAccelerometerUpdateInterval:1.0/2.0]; //Update twice per second
        [self.motionManager startAccelerometerUpdatesToQueue:[NSOperationQueue mainQueue] withHandler:^(CMAccelerometerData *accelerometerData, NSError *error)
         {
self.xAccLabel.text = [NSString stringWithFormat:@"%f", accelerometerData.acceleration.x];
self.yAccLabel.text = [NSString stringWithFormat:@"%f", accelerometerData.acceleration.y];
self.zAccLabel.text = [NSString stringWithFormat:@"%f", accelerometerData.acceleration.z];
         }];
    }
else if ([self.motionManager isAccelerometerActive])
    {
        [self.motionManager stopAccelerometerUpdates];
self.xAccLabel.text = @"...";
self.yAccLabel.text = @"...";
self.zAccLabel.text = @"...";
    }

if ([self.motionManager isGyroAvailable] && ![self.motionManager isGyroActive])
    {
        [self.motionManage rsetGyroUpdateInterval:1.0/2.0];
        [self.motionManager startGyroUpdatesToQueue:[NSOperationQueue mainQueue] withHandler:^(CMGyroData *gyroData, NSError *error)
         {
self.xGyroLabel.text = [NSString stringWithFormat:@"%f", gyroData.rotationRate.x];
self.yGyroLabel.text = [NSString stringWithFormat:@"%f", gyroData.rotationRate.y];
self.zGyroLabel.text = [NSString stringWithFormat:@"%f", gyroData.rotationRate.z];
         }];
    }
else if ([self.motionManager isGyroActive])
    {
        [self.motionManager stopGyroUpdates];
self.xGyroLabel.text = @"...";
self.yGyroLabel.text = @"...";
self.zGyroLabel.text = @"...";
    }

if ([self.motionManager isMagnetometerAvailable] && ![self.motionManager isMagnetometerActive])
    {
        [self.motionManager setMagnetometerUpdateInterval:1.0/2.0];
        [self.motionManager startMagnetometerUpdatesToQueue:[NSOperationQueue mainQueue] withHandler:^(CMMagnetometerData *magData, NSError *error)
         {
self.xMagLabel.text = [NSString stringWithFormat:@"%f", magData.magneticField.x];
self.yMagLabel.text = [NSString stringWithFormat:@"%f", magData.magneticField.y];
self.zMagLabel.text = [NSString stringWithFormat:@"%f", magData.magneticField.z];
         }];
    }
else if ([self.motionManager isMagnetometerActive])
    {
        [self.motionManager stopMagnetometerUpdates];
self.xMagLabel.text = @"...";
self.yMagLabel.text = @"...";
self.zMagLabel.text = @"...";
    }
}

For a little bit of fun, you’ll go ahead and make it so that your toggling is based on the shaking of the device, so you will modify your -shakeDetected: method.

-(void)shakeDetected:(NSNotification *)paramNotification
{
if ([self.statusLabel.text isEqualToString:@"Updating"])
    {
self.statusLabel.text = @"Stopped";
        [self toggleUpdates];
    }
else
    {
self.statusLabel.text = @"Updating";
        [self toggleUpdates];
    }
}

Whenever you deal with the CMMotionManager, you should always make sure that all of your updates are stopped at the end of your application, so you’ll set up your -viewDidUnload: method like so:

- (void)viewDidUnload
{
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];
    }

self.motionManager = nil;
    [self setXAccLabel:nil];
    [self setYAccLabel:nil];
    [self setZAccLabel:nil];
    [self setXGyroLabel:nil];
    [self setYGyroLabel:nil];
    [self setZGyroLabel:nil];
    [self setStatusLabel:nil];
    [self setXMagLabel:nil];
    [self setYMagLabel:nil];
    [self setZMagLabel:nil];
    [superview DidUnload];
}

You should also make sure that your application stops updating whenever it enters the background, so switch over to your application delegate and implement -applicationDidEnterBackground:.

- (void)applicationDidEnterBackground:(UIApplication *)application
{
if ([self.viewController.motionManager isAccelerometerAvailable] && [self.viewController.motionManager isAccelerometerActive])
    {
        [self.viewController.motionManager stopAccelerometerUpdates];
    }
if ([self.viewController.motionManager isGyroAvailable] && [self.viewController.motionManager isGyroActive])
    {
        [self.viewController.motionManager stopGyroUpdates];
    }
if ([self.viewController.motionManager isMagnetometerAvailable] && [self.viewController.motionManager isMagnetometerActive])
    {
        [self.viewController.motionManager stopMagnetometerUpdates];
    }
}

If you run the application now, a quick shake will start updating all your values, resulting in a view similar to that in Figure 12–8.

Image

Figure 12–8. Your application receiving raw device information

The main problem that you will probably notice with this setup is that your data doesn’t really make much sense. It is raw, biased data that isn’t exactly very easy to use. Up next, you’ll change your implementation around to get better using a fourth group of values you can access from CMMotionManager: CMDeviceMotion.

Just like the accelerometer, gyroscope, and magnetometer, you can access CMDeviceMotion by starting and stopping updates using very similar methods: -startDeviceMotionUpdates and -startDeviceMotionUpdatesToQueue:withHandler:. However, you also have two extra methods on top of these that allow you to specify a “reference frame,”-startDeviceMotionUpdatesUsingReferenceFrame: and -startDeviceMotionUpdatesUsingReferenceFrame:toQueue:WithHandler:. We will go over the idea of the reference frame shortly.

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

  1. attitude: This property is an instance of the CMAttitude class, which gives you an incredibly detailed insight into the device’s orientation at a given time as compared to a reference frame. In this class, you can access properties such as roll, pitch, and yaw. These values, measured in radians, allow you an incredibly accurate measurement of your device’s orientation.
  2. rotationRate: This value, measured in radians per second, is just like the previous rotationRate, but gives a more accurate reading by reducing device bias that causes a still device to have nonzero rotation values.
  3. gravity: Represents the acceleration caused solely by gravity on the device
  4. userAcceleration: Represents the physical acceleration imparted on a device by the user outside of gravitational acceleration
  5. magneticField: This value is similar to the one you used before; however, it removes any device bias, resulting in a significantly more accurate reading than you had before.

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 (3.14…). A radian value of pi or 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.

You will go through your code and update it to use data only from the deviceMotion property.

Since you now have two different acceleration-based properties you can access, you’ll go specifically with the gravity property. I’ve changed the top UILabel intheview from “Accelerometer” to “Gravity”, but this is, of course, optional.

Now, your -toggleUpdates method will look like so:

-(void)toggleUpdates
{
if ([self.motionManager isDeviceMotionAvailable] && ![self.motionManager isDeviceMotionActive])
    {
        [self.motionManager setDeviceMotionUpdateInterval:1.0/2.0];
        [self.motionManager startDeviceMotionUpdatesToQueue:[NSOperationQueue mainQueue] withHandler:^(CMDeviceMotion *motion, NSError *error)
         {
self.xAccLabel.text = [NSString stringWithFormat:@"%f", motion.gravity.x];
self.yAccLabel.text = [NSString stringWithFormat:@"%f", motion.gravity.y];
self.zAccLabel.text = [NSString stringWithFormat:@"%f", motion.gravity.z];

self.xGyroLabel.text = [NSString stringWithFormat:@"%f", motion.rotationRate.x];
self.yGyroLabel.text = [NSString stringWithFormat:@"%f", motion.rotationRate.y];
self.zGyroLabel.text = [NSString stringWithFormat:@"%f", motion.rotationRate.z];

self.xMagLabel.text = [NSString stringWithFormat:@"%f", motion.magneticField.field.x];
self.yMagLabel.text = [NSString stringWithFormat:@"%f", motion.magneticField.field.y];
self.zMagLabel.text = [NSString stringWithFormat:@"%f", motion.magneticField.field.z];
         }];
    }
else if ([self.motionManager isDeviceMotionActive])
    {
        [self.motionManager stopDeviceMotionUpdates];

self.xAccLabel.text = @"...";
self.yAccLabel.text = @"...";
self.zAccLabel.text = @"...";
self.xGyroLabel.text = @"...";
self.yGyroLabel.text = @"...";
self.zGyroLabel.text = @"...";
self.xMagLabel.text = @"...";
self.yMagLabel.text = @"...";
self.zMagLabel.text = @"...";
    }
}

You need to also change your -viewDidUnload method like so:

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

self.motionManager = nil;
    [self setXAccLabel:nil];
    [self setYAccLabel:nil];
    [self setZAccLabel:nil];
    [self setXGyroLabel:nil];
    [self setYGyroLabel:nil];
    [self setZGyroLabel:nil];
    [self setStatusLabel:nil];
    [self setXMagLabel:nil];
    [self setYMagLabel:nil];
    [self setZMagLabel:nil];
    [superview DidUnload];
}

In your application delegate, you will also update your -applicationDidEnterBackground: method.

- (void)applicationDidEnterBackground:(UIApplication *)application
{
if ([self.viewController.motionManager isDeviceMotionAvailable] && [self.viewController.motionManager isDeviceMotionActive])
    {
        [self.viewController.motionManager stopDeviceMotionUpdates];
    }
}

So now if you run this application, you will probably notice that most of your values are quite a bit more stable. You may also see all zeros for your magnetometer readings. Move your device in afigure-eight motion to calibrate your magnetometer until these values start updating.

Now that you have switched over to using the DeviceMotion, you will also add in fields to show your pitch, yaw, and roll from your attitude property. Update your view to resemble Figure 12–9, with your new value UILabel properties named rollLabel, pitchLabel, and yawLabel.

Image

Figure 12–9. Your new interface for displaying attitude

Attitude Properties

For your use of the CMAttitude class, you are accessing the three simplest values you can from the class in order to determine device orientation.

  1. roll: Specifies position of rotation around the yaxis
  2. pitch: Specifies position of rotation around the xaxis
  3. yaw: Specifies position of rotation around the zaxis

All three of these values are measured in radians, which means your displayed values will range from 0 to roughly either 3.14 or -3.14 (meaning a rotation ofpi radians, which is equal to 180 degrees).

Now you can update your -toggleUpdates: method again to include the new values to be displayed.

-(void)toggleUpdates
{
if ([self.motionManager isDeviceMotionAvailable] && ![self.motionManager isDeviceMotionActive])
    {
        [self.motionManager setDeviceMotionUpdateInterval:1.0/2.0];
        [self.motionManager startDeviceMotionUpdatesToQueue:[NSOperationQueue mainQueue] withHandler:^(CMDeviceMotion *motion, NSError *error)
         {
self.xAccLabel.text = [NSString stringWithFormat:@"%f", motion.gravity.x];
self.yAccLabel.text = [NSString stringWithFormat:@"%f", motion.gravity.y];
self.zAccLabel.text = [NSString stringWithFormat:@"%f", motion.gravity.z];

self.xGyroLabel.text = [NSString stringWithFormat:@"%f", motion.rotationRate.x];
self.yGyroLabel.text = [NSString stringWithFormat:@"%f", motion.rotationRate.y];
self.zGyroLabel.text = [NSString stringWithFormat:@"%f", motion.rotationRate.z];

self.xMagLabel.text = [NSString stringWithFormat:@"%f", motion.magneticField.field.x];
self.yMagLabel.text = [NSString stringWithFormat:@"%f", motion.magneticField.field.y];
self.zMagLabel.text = [NSString stringWithFormat:@"%f", motion.magneticField.field.z];

//////NEW ATTITUDE CODE
self.rollLabel.text = [NSString stringWithFormat:@"%f", motion.attitude.roll];
self.pitchLabel.text = [NSString stringWithFormat:@"%f", motion.attitude.pitch];
self.yawLabel.text = [NSString stringWithFormat:@"%f", motion.attitude.yaw];
//////END OF NEW CODE
         }];
    }
else if ([self.motionManager isDeviceMotionActive])
    {
        [self.motionManager stopDeviceMotionUpdates];

self.xAccLabel.text = @"...";
self.yAccLabel.text = @"...";
self.zAccLabel.text = @"...";
self.xGyroLabel.text = @"...";
self.yGyroLabel.text = @"...";
self.zGyroLabel.text = @"...";
self.xMagLabel.text = @"...";
self.yMagLabel.text = @"...";
self.zMagLabel.text = @"...";

//////NEW ATTITUDE CODE
self.rollLabel.text = @"...";
self.pitchLabel.text = @"...";
self.yawLabel.text = @"...";
//////END OF NEW CODE
    }
}

Though it’s not exactly required, you will also specify a reference frame for your attitude, so that you have some idea of what your device is being compared to, using the -startDeviceMotionUpdatesUsingReferenceFrame:toQueue:withHandler: method. The reference frame parameter of this method accepts four possible values as of iOS 5.0.

  • CMAttitudeReferenceFrameXArbitraryZVertical: Specifies a reference frame with the zaxis along the vertical and the xaxis along any arbitrary direction; more simply, the device is flat and face-up.
  • CMAttitudeReferenceFrameXArbitraryCorrectedZVertical: This is the same as the previous value, but the magnetometer is used to provide better accuracy. This option increases CPU usage, and also requires the magnetometer to be both available and calibrated.
  • CMAttitudeReferenceFrameXMagneticNorthZVertical: This reference frame has the zaxis vertical as before, but with the xaxis 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:This reference frame is just like the previous, but the xaxis is directed toward “true north,” rather than “magnetic north.” The location of the device must be available in order for the device to be able to calculate the difference between the two.

NOTE: When dealing with the magnetometer, you must be sure to understand the 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, 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 *motion, NSError *error)
         {
self.xAccLabel.text = [NSString stringWithFormat:@"%f", motion.gravity.x];
self.yAccLabel.text = [NSString stringWithFormat:@"%f", motion.gravity.y];
self.zAccLabel.text = [NSString stringWithFormat:@"%f", motion.gravity.z];

self.xGyroLabel.text = [NSString stringWithFormat:@"%f", motion.rotationRate.x];
self.yGyroLabel.text = [NSString stringWithFormat:@"%f", motion.rotationRate.y];
self.zGyroLabel.text = [NSString stringWithFormat:@"%f", motion.rotationRate.z];

self.xMagLabel.text = [NSString stringWithFormat:@"%f", motion.magneticField.field.x];
self.yMagLabel.text = [NSString stringWithFormat:@"%f", motion.magneticField.field.y];
self.zMagLabel.text = [NSString stringWithFormat:@"%f", motion.magneticField.field.z];

self.rollLabel.text = [NSString stringWithFormat:@"%f", motion.attitude.roll];
self.pitchLabel.text = [NSString stringWithFormat:@"%f", motion.attitude.pitch];
self.yawLabel.text = [NSString stringWithFormat:@"%f", motion.attitude.yaw];
         }];

Now, when you run your application, you may start off seeing “0.0” for all your values. If you move your device around in a figure-eight motion to get your magnetometer calibrated, they 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, then at the moment that your x axis is aligned with the Earth’s magnetic field, your yaw value should get very close to zero, as in Figure 12–10.

Image

Figure 12–10. Your application receiving calibrated device information

Recipe 12–3: Moving a UILabel with the Accelerometer

Now that we have gone especially into detail on exactly how to access all of the data the Core Motion can provide, you can create a simple implementation to actually demonstrate its use beyond accessing values. You will simply modify your existing application, so be sure to save a new copy of your project before you continue.

You will be changing your application to utilize the gravity property that you previously accessed in order to move a UILabel around your view. To do this, you will require a very small update interval for your device motion information, so you want to minimize the actual amount of data you display. You will remove all but the statusLabel from your view, so that your user interface will look like Figure 12–11. I have changed the UILabel as well to match the instructions for your new functionality.

When you remove all the other UILabels from your view, you can leave your properties all set up in your header file from your previous recipe if you want. While it takes up a small amount of memory to still have them here, this will be fairly insignificant for your purposes.

Image

Figure 12–11. Your re-simplified user interface for moving a UILabel

Next, you can delete (or comment out) your line from your –toggleUpdates method that resembles the following, which sets your update interval for your CMMotionManager.

[self.motionManager setDeviceMotionUpdateInterval:1.0/2.0];

If you choose not to set an update interval, the device will default to an incredibly small value, meaning that your information will update very frequently.

You will also change your attitude’s reference frame to one that does not require any calibration of the magnetometer so as to improve performance speed. Specifically, you will use the CMAttitudeReferenceFrameXArbitraryCorrectedZVertical value.

Finally, you can add in your code to retrieve the gravity values, and then adjust your UILabel’s frame based on them. Overall, your –toggleUpdates method will look like so:

-(void)toggleUpdates
{
if ([self.motionManager isDeviceMotionAvailable] && ![self.motionManager isDeviceMotionActive])
    {
        [self.motionManager setDeviceMotionUpdateInterval:1.0/2.0];
        [self.motionManager startDeviceMotionUpdatesUsingReferenceFrame:CMAttitudeReferenceFrameXArbitraryCorrectedZVertical toQueue:[NSOperationQueue mainQueue] withHandler:^(CMDeviceMotion *motion, NSError *error)
         {
int scale = 5.0;
CGRect labelRect = self.statusLabel.frame;
             labelRect.origin.x += motion.gravity.x * scale;
if (!CGRectContainsRect(self.view.bounds, labelRect))
             {
                 labelRect.origin.x = self.statusLabel.frame.origin.x;
             }
             labelRect.origin.y -= motion.gravity.y *scale;
if (!CGRectContainsRect(self.view.bounds, labelRect))
             {
                 labelRect.origin.y = self.statusLabel.frame.origin.y;
             }
             [self.statusLabel setFrame:labelRect];
         }];
    }
else if ([self.motionManager isDeviceMotionActive])
    {
        [self.motionManager stopDeviceMotionUpdates];
    }
}

You can also update your -shakeDetected: method to make more sense with your text:

-(void)shakeDetected:(NSNotification *)paramNotification
{
if ([self.statusLabel.text isEqualToString:@"Unlocked"])
    {
self.statusLabel.text = @"Locked";
        [self toggleUpdates];
    }
else
    {
self.statusLabel.text = @"Unlocked";
        [self toggleUpdates];
    }
}

If your application was not linked to the CoreGraphics framework by default, you will run into a linker error, so make sure that this is done before running your app. To do this, the procedure is the same as the one you did earlier with the CoreMotion framework, but you do not need to use any actual #import statements.

Now, your newest application should give you a nice little UILabel that you can move around by tilting the device, a screenshot of which is shown in Figure 12–12.

Image

Figure 12–12. Your application with a UILabel moving based on the orientation of the device

Summary

We have gone into pretty specific detail about accessing the multiple different values and information that the Core Motion framework has to offer. We were able to go from raw data to more calibrated, functional values that we were then able to 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