Chapter     6

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 simply 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 unbelievable. 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 6-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 action 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 created by Apple to handle shake events easily. When a user shakes the device, this message is dispatched to the first responder of your application. The first responder is an object, often a view controller, that receives an event first.

For example, you could set up your application’s main view to receive shake events using code, as shown in Listing 6-1.

Listing 6-1.  One example of receiving shake events

@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 6-1 works for most situations, a slightly more sophisticated solution allows you to recognize shake events from anywhere within 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. You should intercept the event, motionEnded:withEvent:, on the window object and implement your own notification scheme. This will let any observing object in your application know when the device has been shaken.

To start, create a new single view application and title it “Recipe 6-1 Recognizing Shake Events.” Next, 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 6-1).

9781430259596_Fig06-01.jpg

Figure 6-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. Because we’re using the single view application template, which defaults to a storyboard, we’ll need to override the getter method for the window to let the system know we’re using a custom UIWindow. Listing 6-2 shows you how you should modify your AppDelegate.h and AppDelegate.m files.

Listing 6-2.  Modifying AppDelegate to use a custom UIWindow

//
//  AppDelegate.h
//  Recipe 6-1 Recognizing Shake Events
//

#import <UIKit/UIKit.h>
#import "MainWindow.h"

@interface AppDelegate : UIResponder <UIApplicationDelegate>

@property (strong, nonatomic) MainWindow *window;

@end

//
//  AppDelegate.m
//  Recipe 6-1 Recognizing Shake Events
//

#import "AppDelegate.h"

//...

- (MainWindow *)window
{
    if(!_window)
    {
        
        _window=[[MainWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
        
    }
    return _window;
}

//...

Note   If you are using the .xib approach described in Chapter 1, you will need to import the MainWindow.h file in the AppDelegate.h file. Then, you will need to change the didFinishLaunchingWithOptions method shown in Listing 6-2 instead of what is shown in Listing 6-1.

Listing 6-3.  Setting up the application:didFinishLaunchingWithOptions: method without storyboards

- (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.m, add the code in Listing 6-4.

Listing 6-4.  Adding code to mainWindow.m to intercept shake events

@implementation MainWindow

// ...

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

// ...

@end

The code in Listing 6-4 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 recipe uses NOTIFICATION_SHAKE, but you can pick any name you like. An observer pattern is a software design pattern where one object, in this case NSNotificationCenter, notifies its observers when an event occurs, usually by calling one of the observer’s methods. As you will shortly see, the observer will be the view controller.

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 similar to the implementation in Listing 6-5.

Listing 6-5.  Implementation of the NSNotification Observer pattern

@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 in the viewWillDisappear method. This might or might not be what you want. If you want to keep tracking shake events even though the view has disappeared, 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 be tested within the simulator. Simply use the Shake Gesture item under the Hardware menu, as shown in Figure 6-2.

9781430259596_Fig06-02.jpg

Figure 6-2. Shake events can be simulated

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

9781430259596_Fig06-03.jpg

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

As you can see from Figure 6-3, the text “Shaken not stirred” is logged to the screen when the notification center calls the shakeDetected method you provided as a selector.

Recipe 6-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 (such as an iPhone 4 or later) to test the app.

Core Motion Sensors

In Core Motion, you can access three different pieces of hardware on a device, assuming the device is new enough to be equipped with said hardware (see Table 6-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 might also be any other magnetic fields nearby. A magnetic field nearby can interfere with the calibration of the compass. However, this is not permanent; it simply requires a recalibration to fix it.

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

Table 6-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

Note   Although Table 6-1 mentions the iPhone 3GS and the first iPad, the oldest devices iOS 7 supports are the iPhone 4 and iPad 2.

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 6-4 shows the axes and the positive directions of these rotations.

9781430259596_Fig06-04.jpg

Figure 6-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, such as “Raw Motion Data Test.” Because you’re going to use the Core Motion framework, you need to link the CoreMotion.framework binary to your project.

Next, add labels and a button to the main view and organize them so the view resembles Figure 6-5. You will need 21 labels, 9 of which will be outlets. Make sure you make the label widths large enough so they don’t cut off the values too much.

9781430259596_Fig06-05.jpg

Figure 6-5. Main view interface

Your app will update the labels containing values (in other words, the ones with the text 0.0 in Figure 6-4). Therefore, create outlets for those nine labels so you’ll be able to change their text at runtime.

Give the outlets and actions the following names:

  • Accelerometer value labels: xAccLabel, yAccLabel, and zAccLabel
  • Gyroscope value labels: xGyroLabel, yGyroLabel, and zGyroLabel
  • Magnetometer value labels: xMagLabel, yMagLabel, and zMagLabel
  • Button outlet: startButton
  • Button action: toggleUpdates

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

When done, your view controller interface declaration should resemble Listing 6-6.

Listing 6-6.  The view controller header file with all actions and outlets

@interface ViewController : UIViewController

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

@property (weak, nonatomic) IBOutlet UIButton *startButton;

- (IBAction)toggleUpdates:(id)sender;

@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, a property that initializes when it’s needed rather than immediately, to access a single instance of that class.

Make the changes in Listing 6-7 to the view controller’s header class. Note that the outlet properties have been removed for the sake of brevity, so don’t remove them in your code.

Listing 6-7.  Importing the CoreMotion framework and creating a CMMotionManager property

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

@interface ViewController : UIViewController

// ...

@property (strong, nonatomic) CMMotionManager *motionManager;

- (IBAction)toggleUpdates:(id)sender;
;

@end

Now switch to ViewController.mand add the code in Listing 6-8 to the view controller’s implementation section. Again, code has been removed for brevity.

Listing 6-8.  Implementing the lazy initialization property

@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. 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 6-6).

9781430259596_Fig06-06.jpg

Figure 6-6. Setting supported interface orientations

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

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

For example, we can use these steps to set up the accelerometer, as shown in Listing 6-9 (don’t start adding code yet).

Listing 6-9.  Example code for setting up the accelerometer

// 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 when it updates. 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. Listing 6-10 shows methods that start and stop all three sensors at once. Add them to your project view controller implementation file.

Listing 6-10.  Implementation to start and stop all three sensors

- (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 we will start and stop the motion services from a button with a toggle method tied to our button. To do this, modify the two methods in the ViewController.m, as shown in Listing 6-11.

Listing 6-11.  Modifying the viewDidLoad and toggleUpdates: methods to support the start and stop button

- (void)viewDidLoad
{
    [super viewDidLoad];
        // Do any additional setup after loading the view, typically from a nib.
    
    [self.startButton setTitle:@"Stop" forState:UIControlStateSelected];
    [self.startButton setTitle:@"Start" forState:UIControlStateNormal];
}

//...

- (IBAction)toggleUpdates:(id)sender {
    
    if(![self.startButton isSelected])
    {
        [self startUpdates];
        [self.startButton setSelected:YES];
    }
    else
    {
        [self stopUpdates];
        [self.startButton setSelected:NO];
    }
    
}

Your app is 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 6-7 shows a screen shot of the app in action.

9781430259596_Fig06-07.jpg

Figure 6-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.

To make the concept of “pushing” clear, think of your email system. With “push” enabled, an email will show up every time one is available. This, of course, can cause battery drain as well as increase data consumption. 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. Using the email analogy, this is more of a manual process where the user has to check email first to get the update. Thus, data is provided only when it’s needed.

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 an appropriate time, retrieve the values from those properties, as Listing 6-12 shows.

Listing 6-12.  An example of using the pull mode to access accelerometer data

// 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 6-2 provides a general guideline for update intervals.

Table 6-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 use 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 might have discovered how incomprehensible the data from the sensors are. This is because the data from the sensors are biased; that is, they are affected by more than one force. The accelerometer, for example, 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 such as high-pass 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 device’s real orientation and motion. Recipe 6-3 shows you how to use this convenient feature.

Recipe 6-3: Accessing Device Motion Data

The preceding 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 difficult work for you, ready to be utilized through 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 preceding 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 will be 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 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 6-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 preceding 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 6-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 6-2. So go ahead and create a new single view application project and link 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 such as the one in Figure 6-8. You’ll need 35 label objects, 15 of which are displaying values. You will also need a button. Because you will update those labels at runtime, you’ll need to create outlets for them. Use the following names for the outlets and actions:

  • 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
  • Button outlet: startButton
  • Button action: toggleUpdates

9781430259596_Fig06-08.jpg

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

As you did in Recipe 6-2, add a motion manager property to the view controller interface file, as shown in Listing 6-13.

Listing 6-13.  Adding the CMMotionManager and CoreMotion import statements to the view controller interface

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

@interface ViewController : UIViewController

// ...

@property (strong,nonatomic) CMMotionManager *motionManager;

- (IBAction)toggleUpdates:(id)sender;

@end

The property should have the same lazy initialization implementation as in the preceding recipe. This is shown again in Listing 6-14.

Listing 6-14.  Implementing the lazy initialization property

@implementation ViewController

// ...

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

// ...

@end

Also, as in Recipe 6-2, your app should support only portrait orientation (refer to Figure 6-6).

Accessing Device Motion Data

Starting and stopping updates and retrieving 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 need only one start statement, which allows you to reach all the data at once.

So, add the methods in Listing 6-15 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.

Listing 6-15.  Implementing the startUpdates and stopUpdates methods

- (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 toggleUpdates method as we did in Recipe 6-2 and update the viewDidLoad method, as shown in Listing 6-16.

Listing 6-16.  Updating the viewDidLoad and toggleUpdates: methods to add start and stop functionality

- (void)viewDidLoad
{
    [super viewDidLoad];
        // Do any additional setup after loading the view, typically from a nib.
    
    [self.startButton setTitle:@"Stop" forState:UIControlStateSelected];
    [self.startButton setTitle:@"Start" forState:UIControlStateNormal];

}
//...

(IBAction)toggleUpdates:(id)sender
{
    
    if(![self.startButton isSelected])
    {
        [self startUpdates];
        [self.startButton setSelected:YES];
    }
    else
    {
        [self stopUpdates];
        [self.startButton setSelected:NO];
    }
}

//...

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 6-2. You might 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 might be handy if you would like to switch the coordinate system or use magnetic north versus true north. Changing the reference frame 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 references 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   As mentioned earlier in this chapter, 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 it 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 as the direction accuracy doesn’t matter that much and using location services is more than is needed. Change your call to the startDeviceMotionUpdatesToQueue:withHandler: method, as shown in Listing 6-17.

Listing 6-17.  Adding magnetic north reference to the startDeviceMotionUpdatesToQueue:withHandler method

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

When you run your application, you might see “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.

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 your x-axis is aligned with the Earth’s magnetic field, your yaw value should get close to zero, as in Figure 6-9.

9781430259596_Fig06-09.jpg

Figure 6-9. Your application receiving calibrated-device information

Recipe 6-4: Moving a Label with Gravity

Recipes 6-2 and 6-3 showed you how to access the various Core Motion data. It’s time to put that knowledge to 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, as shown in Listing 6-18.

Listing 6-18.  Setting up the ViewController.h file

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

@interface ViewController : UIViewController

@property (strong, nonatomic) CMMotionManager *motionManager;

@end

Now switch to ViewController.m and implement the property using lazy initialization; add stubs for the startUpdate and stopUpdate methods. Listing 6-19 shows this code.

Listing 6-19.  Creating the custom initializer and adding method stubs

@implementation ViewController

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

- (void)startUpdates
{
    
}

- (void)stopUpdates
{
    
}

// ...

@end

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

Instead of creating a button in this recipe, we’ll start the updates when the view loads. So add the code in Listing 6-20 to the viewDidLoad method in your viewController.m file.

Listing 6-20.  Modifying the viewDidLoad method to start updates

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
        
    [self startUpdates];
}

Next, you should stop the updates when the view goes away, so add the code in Listing 6-21 right after the viewDidLoad method.

Listing 6-21.  Implementing the viewWillDisappear method to stop updates

-(void)viewWillDisappear:(BOOL)animated
{
    
    [self stopUpdates];
}

Finally, go to the Main.storyboard and add a label and an outlet for it so 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 6-10.

9781430259596_Fig06-10.jpg

Figure 6-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. Implement the startUpdates and stopUpdates methods, as shown in Listing 6-22.

Listing 6-22.  The startUpdates method and stopUpdates method implementation

- (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. It uses the deviceMotion.gravity property as a velocity value (although it’s actually an acceleration) and calculates the delta movement from it. Because each value only reaches between -1.0 and 1.0, 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

Adjust the startUpdates method to implement a simple acceleration algorithm. Change your code according to Listing 6-23.

Listing 6-23.  Adding an acceleration algorithm to the startUpdates method

- (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 might 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. In addition, 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.

Also notable 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 you like best.

Lastly, but important, if the label reaches a border you reset the speed, as shown in Listing 6-23. Otherwise, it will keep 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 details about accessing the multiple values and information that the Core Motion framework has to offer. You started with raw data and then used 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