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 apps—applications 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).
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.
Figure 5-2. Shake events can be simulated
Figure 5-3 shows the test application after it has responded to a shake event.
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).
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.
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.
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.
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.
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).
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:
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.
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 biased—that 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.
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:
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:
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.
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.
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.
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.