An accelerometer measures the linear acceleration of the device. The original iPhone, and first generation iPod touch, use the LIS302DL 3-axis MEMS based accelerometer produced by STMicroelectronics. Later iPhone and iPod touch models use a similar LIS331DL chip, also manufactured by STMicroelectronics.
Both of these accelerometers can operate in two modes, allowing the chip to measure either ±2g and ±8g. In both modes the chip can sample at either 100 Mhz or 400 Mhz. Apple operates the accelerometer in the ±2g mode (presumably at 100 Mhz) with a nominal resolution of 0.018g. In the ±8g mode the resolution would be four times coarser, and the presumption must be that Apple decided better resolution would be more useful than a wider range. Under normal conditions the device will actually measure g-forces to approximately ±2.3g however measurements above a 2g are uncalibrated.
While it should in theory be possible to change the operating mode of the accelerometer, there is currently no published API that allows you to do so within the SDK.
The iPhone’s accelerometer measures the linear acceleration of the device so it can report the device’s roll and pitch, but not its yaw. If you are dealing with a device that has a digital compass you can combine the accelerometer and magnetometer readings to have roll, pitch, and yaw measurements (see Chapter 5 for details on how to access the magnetometer).
Yaw, pitch, and roll refer to the rotation of the device in three axes. If you think about an aircraft in the sky, pushing the nose down or pulling it up modifies the pitch angle of the aircraft. However, if you keep the nose straight ahead you can also modify the roll of the aircraft using the flaps; one wing will come up, the other will go down. By keeping the wings level you can use the tail flap to change the heading (or yaw) of the aircraft, rotating it in a 2D plane.
The accelerometer reports three figures: X, Y, and Z (see Figure 4-1). Acceleration values for each axis are reported directly by the hardware as G-force values. Therefore, a value of 1.0 represents a load of approximately 1-gravity (Earth’s gravity). X corresponds to roll, Y to pitch, and Z to whether the device is front side up or front side down, with a value of 0.0 being reported when the iPhone is edge-on.
When dealing with acceleration measurements you must keep in mind that the accelerometer is measuring just that: the linear acceleration of the device. When at rest (in whatever orientation) the figures represent the force of gravity acting on the device, and correspond to the roll and pitch of the device (in the X and Y directions at least). But while in motion, the figures represent the acceleration due to gravity, plus the acceleration of the device itself relative to its rest frame.
Let’s go ahead and implement a simple application to illustrate how to approach the accelerometer. Open Xcode and start a new View-based application for the iPhone, and name the project “Accelerometer” when prompted for a filename.
The raw accelerometer data can also be accessed using the Core
Motion framework, which was new in iOS 4.0. I talk about how to do this
in Chapter 6. It is
therefore possible, even likely, that the UIAccelerometer
class discussed in this
chapter my be deprecated in a future iOS release.
Click on the AccelerometerViewController.xib file to open it
into Interface Builder. Since you want to both report the raw figures from
the accelerometer and also display them using a progress bar, go ahead and
drag and drop three UIProgressView
controls from the Object Library into the View window. Then add two
UILabel
elements for each progress bar:
one to hold the X, Y, or Z label and the other to hold the accelerometer
measurements. After you do that, the view should look something a lot like
Figure 4-2.
Go ahead and close the Utilities panel and click to open the
Assistant Editor. Then Control-Click and drag from the three UIProgressView
elements, and the three UILabel
elements to the AcclerometerViewController.h header file. The
header file should be displayed in the Assistant Editor on the right-hand
side of the Xcode 4 interface (see Figure 4-3).
This will automatically create and declare three UILabel
and three UIProgressView
variables as IBOutlet
objects. Since they aren’t going to be
used outside the class, there isn’t much point in declaring them as class
properties, which you’d do by Control-click and drag from the element to
outside the curly brace. After doing this the code should look like
this:
#import <UIKit/UIKit.h> @interface AccelerometerViewController : UIViewController { IBOutlet UIProgressView *xBar; IBOutlet UIProgressView *yBar; IBOutlet UIProgressView *zBar; IBOutlet UILabel *xLabel; IBOutlet UILabel *yLabel; IBOutlet UILabel *zLabel; } @end
Close the Assistant Editor, return to the Standard Editor and click
on the AccelerometerViewController.h
interface file. Now go ahead and set up a UIAccelerometer
instance. Also declare the class
as a UIAccelerometerDelegate
. Here’s
how the should look when you are done:
#import <UIKit/UIKit.h> @interface AccelerometerViewController : UIViewController <UIAccelerometerDelegate> { IBOutlet UILabel *xLabel; IBOutlet UILabel *yLabel; IBOutlet UILabel *zLabel; IBOutlet UIProgressView *xBar; IBOutlet UIProgressView *yBar; IBOutlet UIProgressView *zBar; UIAccelerometer *accelerometer; } @end
Make sure you’ve saved your changes and click on the corresponding AccelerometerViewController.m implementation file to open it in the Xcode editor. You don’t actually have to do very much here, as Interface Builder handled most of the heavy lifting by adding code to properly handle the user interface elements. Here’s what the file should look like when you are done:
#import "AccelerometerViewController.h" @implementation AccelerometerViewController - (void)viewDidLoad { accelerometer = [UIAccelerometer sharedAccelerometer]; accelerometer.updateInterval = 0.1; accelerometer.delegate = self; [super viewDidLoad]; } - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)viewDidUnload { [xBar release]; xBar = nil; [yBar release]; yBar = nil; [zBar release]; zBar = nil; [xLabel release]; xLabel = nil; [yLabel release]; yLabel = nil; [zLabel release]; zLabel = nil; [super viewDidUnload]; } - (void)dealloc { [xLabel release]; [yLabel release]; [zLabel release]; [xBar release]; [yBar release]; [zBar release]; accelerometer.delegate = nil; [accelerometer release]; [super dealloc]; } - (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)interfaceOrientation { return (interfaceOrientation == UIInterfaceOrientationPortrait); } #pragma mark UIAccelerometerDelegate Methods - (void)accelerometer:(UIAccelerometer *)meter didAccelerate:(UIAcceleration *)acceleration { xLabel.text = [NSString stringWithFormat:@"%f", acceleration.x]; xBar.progress = ABS(acceleration.x); yLabel.text = [NSString stringWithFormat:@"%f", acceleration.y]; yBar.progress = ABS(acceleration.y); zLabel.text = [NSString stringWithFormat:@"%f", acceleration.z]; zBar.progress = ABS(acceleration.z); } @end
The UIAccelerometer
is a
singleton object, so we grab a reference to the singleton rather than
allocate and initialize a new instance of the class.
We set the update interval to 0.1, hence the accelerometer:didAccelerate:
method will be
called 10 times every second.
We declare that this class is the delegate for the UIAccelerometer
.
We implement the accelerometer:didAccelerate:
delegate method
and use it to set the X, Y, and Z labels to the raw accelerometer
readings each time it is called. The progress bar values are set to
the absolute value (the value without regard to sign) of the
accelerometer reading.
OK, you’re done. Before you click the Run button, make sure you’ve configured the project to deploy onto your iPhone or iPod touch to test it. Since this application makes use of the accelerometer, and iPhone Simulator doesn’t have one, you’re going to have to test it directly on the device.
If all goes well, you should see something that looks a lot like Figure 4-4.
Apple provide an easy way of determining the device orientation, a
call to UIDevice
will return the
current orientation of the device:
UIDevice *device = [UIDevice currentDevice]; UIDeviceOrientation orientation = device.orientation;
This call will return a UIDeviceOrientation
that can be: UIDeviceOrientationUnknown
, UIDeviceOrientationPortrait
, UIDeviceOrientationPortraitUpsideDown
, UIDeviceOrientationLandscapeLeft
, UIDeviceOrientationLandscapeRight
or UIDeviceOrientationFaceUp
. The sensor underlying
this call is the accelerometer, and you’ll see later in this chapter how
to retrieve the device orientation directly from the raw accelerometer
readings.
As of the time of writing under iOS 4.3 the device does not
correctly report a proper orientation when your application is first
launched, with UIDevice
returning
null
when queried.
Lets go ahead and modify the Accelerometer application to display the device orientation. Click on the AccelerometerViewController.h interface file to open it in the Standard Editor and add the following code, highlighted below, to the class interface:
@interface AccelerometerViewController : UIViewController <UIAccelerometerDelegate> { IBOutlet UILabel *xLabel; IBOutlet UILabel *yLabel; IBOutlet UILabel *zLabel; IBOutlet UIProgressView *xBar; IBOutlet UIProgressView *yBar; IBOutlet UIProgressView *zBar; IBOutlet UILabel *orientationLabel; UIAccelerometer *accelerometer; } - (NSString *)stringFromOrientation:(UIDeviceOrientation) orientation; @end
We’re going to display the current orientation using in a UILabel
, so we’re going to have to write a
convenience method stringFromOrienation:
to convert the UIDeviceOrientation
type returned by the
UIDevice
to an NSString
to display in that label.
Make sure you’ve saved your changes, and click on the corresponding AccelerometerViewController.m implementation file and add the following method:
- (NSString *)stringFromOrientation:(UIDeviceOrientation) orientation { NSString *orientationString; switch (orientation) { case UIDeviceOrientationPortrait: orientationString = @"Portrait"; break; case UIDeviceOrientationPortraitUpsideDown: orientationString = @"Portrait Upside Down"; break; case UIDeviceOrientationLandscapeLeft: orientationString = @"Landscape Left"; break; case UIDeviceOrientationLandscapeRight: orientationString = @"Landscape Right"; break; case UIDeviceOrientationFaceUp: orientationString = @"Face Up"; break; case UIDeviceOrientationFaceDown: orientationString = @"Face Down"; break; case UIDeviceOrientationUnknown: orientationString = @"Unknown"; break; default: orientationString = @"Not Known"; break; } return orientationString; }
Once you have added the stringFromOrienation:
method, add the following
code, highlighted below, to the existing accelerometer:didAccelerate:
method in the same
class:
- (void)accelerometer:(UIAccelerometer *)meter didAccelerate:(UIAcceleration *)acceleration { xLabel.text = [NSString stringWithFormat:@"%f", acceleration.x]; xBar.progress = ABS(acceleration.x); yLabel.text = [NSString stringWithFormat:@"%f", acceleration.y]; yBar.progress = ABS(acceleration.y); zLabel.text = [NSString stringWithFormat:@"%f", acceleration.z]; zBar.progress = ABS(acceleration.z); UIDevice *device = [UIDevice currentDevice]; orientationLabel.text = [self stringFromOrientation:device.orientation]; }
Make sure you’ve saved your changes, and click on the AccelerometerViewController.xib file to open it
in Interface Builder. Drag and drop a UILabel
from the Object Library into the View.
Go ahead and resize and center up the text using the Attributes inspector
from the Utilities panel.
Close the Utilities panel and open the Assistant Editor, which
should show the corresponding interface file for the view controller.
Control-click and drag and connect the UILabel
element to the orientationLabel
outlet in your code, as in
Figure 4-5.
Save your changes, and click Run button in the Xcode toolbar to compile and deploy your application to your device. If all goes well you should see something much like Figure 4-6. As you move the device around, the label will update itself to reflect the current device orientation.
Instead of querying UIDevice
you can use the raw accelerometer readings to determine the device
orientation directly using the atan2
function as shown below:
float x = -[acceleration x]; float y = [acceleration y]; float angle = atan2(y, x);
For any real arguments x and y that are not both equal to zero, atan2(y, x) is the angle in radians between the positive x-axis of a plane and the point given by the specified coordinates on it. The angle is positive for counter-clockwise angles, and negative for clockwise angles.
Let’s go ahead and modify the accelerometer:didAccelerate:
method to
calculate the orientation. Click on the AccelerometerViewController.m implementation
file to open it in the Standard Editor and replace these lines:
UIDevice *device = [UIDevice currentDevice]; orientationLabel.text = [self stringFromOrientation:device.orientation];
with the code highlighted below:
- (void)accelerometer:(UIAccelerometer *)meter didAccelerate:(UIAcceleration *)acceleration { xLabel.text = [NSString stringWithFormat:@"%f", acceleration.x]; xBar.progress = ABS(acceleration.x); yLabel.text = [NSString stringWithFormat:@"%f", acceleration.y]; yBar.progress = ABS(acceleration.y); zLabel.text = [NSString stringWithFormat:@"%f", acceleration.z]; zBar.progress = ABS(acceleration.z); float x = -[acceleration x]; float y = [acceleration y]; float angle = atan2(y, x); if(angle >= −2.25 && angle <= −0.75) { orientationLabel.text = [self stringFromOrientation:UIInterfaceOrientationPortrait]; } else if(angle >= −0.75 && angle <= 0.75){ orientationLabel.text = [self stringFromOrientation:UIInterfaceOrientationLandscapeRight]; } else if(angle >= 0.75 && angle <= 2.25) { orientationLabel.text = [self stringFromOrientation:UIInterfaceOrientationPortraitUpsideDown]; } else if(angle <= −2.25 || angle >= 2.25) { orientationLabel.text = [self stringFromOrientation:UIInterfaceOrientationLandscapeLeft]; } }
If you save your changes, and click on the Run button to rebuild and deploy your application onto your device, there should see little or no change in the application’s operation. However, having access to each component of the orientation opens up many opportunities for creating tilt-based controls.
In addition to directly querying the UIDevice
object for the current orientation, a
program can request to be notified of changes in the device’s
orientation by registering itself as an observer.
We can once again modify the Accelerometer application to make use
of this feature. Open the AccelerometerViewController.m file in the
Standard Editor and delete the code added in the previous section from
the accelerometer:didAccelerate:
method.
If you quickly rebuild the application at this point and deploy it
to your device you will see that the UILabel
now reads “Label” and will no longer
be updated as the device orientation changes.
Once you’ve confirmed that, add the following method:
-(void) viewWillAppear:(BOOL) animated{ [super viewWillAppear:animated]; [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(receivedRotation:) name:UIDeviceOrientationDidChangeNotification object:nil]; }
Here we ask the UIDevice
class
to start generating orientation notifications, and we register the view
controller as an observer. Next add the selector method, shown
below:
-(void) receivedRotatation:(NSNotification*) notification { UIDevice *device = [UIDevice currentDevice]; orientationLabel.text = [self stringFromOrientation:device.orientation]; }
Here we simply update the UILabel
with the device orientation every time
a UIDeviceOrientationDidChangeNotification
event
is received.
Finally we need to remember to remove the program as an observer to stop the generation of messages during the tear down of our view controller. Add the following method to your code:
-(void) viewWillDisappear:(BOOL) animated{ [super viewWillDisappear:animated]; [[NSNotificationCenter defaultCenter] removeObserver: self]; [[UIDevice currentDevice] endGeneratingDeviceOrientationNotifications]; }
If you once again save your changes and click on the Run button to rebuild and deploy the application to your device, you will again see little or no change in the application’s operation.
A useful thing to know a lot of the time is the answer to the question “which way is up?” You can use the same method used earlier to determine the device orientation and graphically show this in the View.
First you’re going to need an image of an arrow. Download, or draw in the graphics package of your choice, an image of an arrow pointing to the left on a transparent background. Save it as, or convert it to, a PNG format file. Drag-and-drop this into your Xcode Project remembering to tick the “Copy items into destination group’s folder (if needed)” check box in the pop up dialog that appears when you drop the files into Xcode (see Figure 4-7).
Click on the AccelerometerViewController.xib file to open
it, and drag-and-drop a UIImageView
from the Object Library onto your View. Position it below the three
UIProgressBar
elements, and resize
the bounding box to be a square using the Size inspector of the Utility
Pane. In the Attributes inspector of the Utility Pane, change the Image
property to be the arrow image that you added to your project. Set the
View mode to be “Aspect Fit”. Uncheck the “Opaque” box in the Drawing
section so that the arrow is rendered correctly with a transparent
background. Finally, use the “Image” drop-down to select the arrow.png
image to be displayed in the UIImageView
(see Figure 4-8).
Close the Utility Pane and open the Assistant Editor.
Control-click and drag from the UIImageView
in your View to the arrowImage
outlet in the Assistant Editor, as
in Figure 4-9, and add an arrrowImage
outlet.
After doing so your interface file should look as below:
@interface AccelerometerViewController :
UIViewController <UIAccelerometerDelegate> {
IBOutlet UILabel *xLabel;
IBOutlet UILabel *yLabel;
IBOutlet UILabel *zLabel;
IBOutlet UIProgressView *xBar;
IBOutlet UIProgressView *yBar;
IBOutlet UIProgressView *zBar;
IBOutlet UIImageView *arrowImage;
IBOutlet UILabel *orientationLabel;
UIAccelerometer *accelerometer;
}
Close the Assistant Editor and switch to the Standard Editor. Go
ahead and click on the AccelerometerViewController.m implementation
file. Add the code highlighted below to the accelerometer:didAccelerate:
method:
- (void)accelerometer:(UIAccelerometer *)meter didAccelerate:(UIAcceleration *)acceleration { xLabel.text = [NSString stringWithFormat:@"%f", acceleration.x]; xBar.progress = ABS(acceleration.x); yLabel.text = [NSString stringWithFormat:@"%f", acceleration.y]; yBar.progress = ABS(acceleration.y); zLabel.text = [NSString stringWithFormat:@"%f", acceleration.z]; zBar.progress = ABS(acceleration.z); float x = -[acceleration x]; float y = [acceleration y]; float angle = atan2(y, x); [arrowImage setTransform:CGAffineTransformMakeRotation(angle)]; }
That’s it. Save your changes again and click on the Run button to compile and deploy the application to your device. Keep the device face towards you and rotate it in flat plane, you should see that the arrow moves as you do so, keeping its orientation pointing upwards (see Figure 4-9).
Apple provides convenience methods to determine whether the current device orientation is portrait:
UIDevice *device = [UIDevice currentDevice]; UIDeviceOrientation orientation = device.orientation; BOOL portrait = UIDeviceOrientationIsPortrait( orientation );
or landscape:
BOOL landscape = UIDeviceOrientationIsLandscape( orientation );
These methods return YES
if the
device is in portrait or landscape mode respectively; otherwise they
return NO
.
Apple’s shake-detection algorithm analyses eight to ten successive
pairs of raw accelerometer triplet values and determines the angle between
these readings. If the change in angular velocity between successive data
points is large then the algorithm determines that a UIEventSubtypeMotionShake
has occurred, and the
motionBegan:withEvent:
delegate method
is called. Conversely, if the change in angular velocity is small and a
shake event has been triggered, the motionEnded:withEvent:
delegate method is
called.
The iPhone is better at detecting side-to-side rather than front-to-back or up-and-down motions. Take this into account in the design of your application.
There are three motion delegate methods, mirroring the methods for
gesture handling: motionBegin:withEvent:
, motionEnded:withEvent:
and motionCancelled:withEvent:
. The first indicates
the start of a motion event, the second the end of this event. You cannot
generate a new motion event for a second (or two) following the first
event. The final delegate method is called when a motion is interrupted by
a system event, such as an incoming phone call.
Let’s go ahead and add shake detection to our Accelerometer
application. You’ll need to add another UILabel
to the UI that will change depending on
the motion event status. Click on the AccelerometerViewController.h interface file to
open it in the Standard Editor and add another UILabel
marked as an IBOutlet
to the class definition:
@interface AccelerometerViewController : UIViewController <UIAccelerometerDelegate> {
IBOutlet UILabel *xLabel;
IBOutlet UILabel *yLabel;
IBOutlet UILabel *zLabel;
IBOutlet UIProgressView *xBar;
IBOutlet UIProgressView *yBar;
IBOutlet UIProgressView *zBar;
IBOutlet UILabel *orientationLabel;
IBOutlet UILabel *shakeLabel;
IBOutlet UIImageView *arrowImage;
UIAccelerometer *accelerometer;
}
Save your changes and click on the AccelerometerViewController.m implementation file to open it in the Xcode editor.
The easiest way to ensure that the view controller receives motion
events is to promote it to First Responder in the viewDidAppear:
method. Remember to make the
controller resign as first responder when the view goes away. Add the
viewDidAppear:
method and modify the
existing viewWillDisappear:
method as
highlighted below. Use the canBecomeFirstResponder
method to indicate that the view controller can indeed become the First
Responder:
- (BOOL)canBecomeFirstResponder { return YES; } - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; [self becomeFirstResponder]; } -(void) viewWillDisappear: (BOOL) animated{ [super viewWillDisappear:animated]; [[NSNotificationCenter defaultCenter] removeObserver: self]; [[UIDevice currentDevice] endGeneratingDeviceOrientationNotifications]; [self resignFirstResponder]; }
Save your changes and click on the AccelerometerViewController.xib file for the
last time. Drag-and-drop a UILabel
into
the View from the Object Library and connect it to the shakeLabel
outlet as in Figure 4-11.
Save your changes and return to the AccelerometerViewController.m file in the editor, and add the following delegate methods to the implementation:
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event { if ( motion == UIEventSubtypeMotionShake ) { shakeLabel.text = @"SHAKE"; shakeLabel.textColor = [UIColor redColor]; } return; } - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event { if ( motion == UIEventSubtypeMotionShake ) { shakeLabel.text = @"NO SHAKE"; shakeLabel.textColor = [UIColor greenColor]; } return; } - (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event { if ( motion == UIEventSubtypeMotionShake ) { shakeLabel.text = @"SHAKE CANCELLED"; shakeLabel.textColor = [UIColor blackColor]; } return; }
Save your changes and click on the Run button in the Xcode toolbar. After the application is built and deployed to your device, try shaking the phone. You should see something very much like Figure 4-12.