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.

Get Basic Sensors in iOS now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.