O'Reilly logo

Basic Sensors in iOS by Alasdair Allan

Stay ahead with the world's most comprehensive technology and business learning platform.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, tutorials, and more.

Start Free Trial

No credit card required

Chapter 4. Using the Accelerometer

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.

Note

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.

About the Accelerometer

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

Note

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.

The iPhone accelerometer axes
Figure 4-1. The iPhone accelerometer axes

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.

Writing an Accelerometer Application

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.

Warning

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.

The Accelerometer application UI
Figure 4-2. The Accelerometer application UI

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

Connecting the UI elements to your code in Interface Builder
Figure 4-3. Connecting the UI elements to your code in Interface Builder

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> { 1

    IBOutlet UILabel *xLabel;
    IBOutlet UILabel *yLabel;
    IBOutlet UILabel *zLabel;

    IBOutlet UIProgressView *xBar;
    IBOutlet UIProgressView *yBar;
    IBOutlet UIProgressView *zBar;

    UIAccelerometer *accelerometer;
}

@end
1

Here we declare that the class implements the UIAccelerometer delegate protocol.

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];1
    accelerometer.updateInterval = 0.1;2
    accelerometer.delegate = self;3
    [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 4
{
    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
1

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.

2

We set the update interval to 0.1, hence the accelerometer:didAccelerate: method will be called 10 times every second.

3

We declare that this class is the delegate for the UIAccelerometer.

4

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.

The Accelerometer application running on an iPhone 4 sitting face-up on my desk, measuring a 1-gravity acceleration straight down
Figure 4-4. The Accelerometer application running on an iPhone 4 sitting face-up on my desk, measuring a 1-gravity acceleration straight down

Determining Device Orientation

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.

Warning

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.

Connecting the orientation outlet to the UI
Figure 4-5. Connecting the orientation outlet to the UI

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.

The Accelerometer application reporting the device orientation
Figure 4-6. The Accelerometer application reporting the device orientation

Determining Device Orientation Directly Using the Accelerometer

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);

Note

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.

Obtaining Notifications when Device Orientation Changes

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.

Which Way Is Up?

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

Adding an arrow image to the Accelerometer project
Figure 4-7. Adding an arrow image to the Accelerometer project

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

Adding the UIImageView to your interface
Figure 4-8. Adding the UIImageView to your interface

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.

Adding a outlet to your code
Figure 4-9. Adding a outlet to your code

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

The arrow points upwards (device held vertically in front of the user)
Figure 4-10. The arrow points upwards (device held vertically in front of the user)

Convenience Methods for Orientation

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.

Detecting Shaking

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.

Note

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.

Connecting the shakeLabel outlet
Figure 4-11. Connecting the shakeLabel outlet

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.

Shake detection on the iPhone
Figure 4-12. Shake detection on the iPhone

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, interactive tutorials, and more.

Start Free Trial

No credit card required