The magnetometer is a magnetoresistive permalloy sensor found in the iPhone 3GS, iPhone 4 and iPad 2, in addition to the accelerometer. The iPhone 3GS uses the AN-203 integrated circuit produced by Honeywell, while the iPhone 4 and iPad 2 make use of the newer AKM8975 produced by AKM Semiconductor. The sensor is located towards the top right hand corner of the device, and measures fields within a ±2 gauss (200 microtesla) range, and is sensitive to magnetic fields of less than 100 microgauss (0.01 microtesla).
The Earth’s magnetic field is roughly 0.6 gauss (60 microtesla). The field around a rare earth magnet can be 14,000 gauss or more.
The magnetometer measures the strength of the magnetic field surrounding the device. In the absence of any strong local fields, these measurements will be of the ambient magnetic field of the Earth, allowing the device to determine its “heading” with respect to the geomagnetic North Pole and act as a digital compass. The geomagnetic heading and true heading relative to the geographical North Pole can vary widely, by several tens of degrees depending on your location.
Combining the heading (yaw) information (see Figure 5-1) returned by this device with the roll and pitch information returned by the accelerometer will let you determine the true orientation of the device in real time.
As well as reporting the current location, the CLLocationManager
class can, in the case where
the hardware supports it, report the current heading of the device. If
location updates are also enabled, the location manager returns both true
heading and magnetic heading values. If location updates are not enabled,
the location manager returns only the magnetic heading value.
Magnetic heading updates are available even if the user has switched off location updates in the Settings application. Additionally, users are not prompted to give permission to use heading data, as it is assumed that magnetic heading information cannot compromise user privacy. On an enabled device the magnetic heading data should therefore always be available to your application.
As mentioned previously, the magnetometer readings will be affected
by local magnetic fields, so the CLLocationManager
may attempt to calibrate its
heading readings by displaying a heading calibration panel (see Figure 5-2) before it starts to issue
update messages.
However, before it does so it will call the locationManagerShouldDisplayHeadingCalibration:
delegate method:
- (BOOL)locationManagerShouldDisplayHeadingCalibration: (CLLocationManager *)manager { return YES; }
If you return YES
from this
method, the CLLocationManager
will pop
up the calibration panel on top of the current window. The calibration
panel prompts the user to move the device in a figure-eight pattern so
that Core Location can distinguish between the Earth’s magnetic field and
any local magnetic fields. The panel will remain visible until calibration
is complete or until you dismiss it by calling the dismissHeadingCalibrationDisplay:
method in the
CLLocationManager
class.
Let’s go ahead and implement a simple view-based application to illustrate how to use the magnetometer. Open Xcode and start a new iPhone project, select a View-based Application template, and name the project “Compass” when prompted for a filename.
Since you’ll be making use of the Core Location framework, the first thing you need to do is add it to our new project. Click on the Compass project file in the Project navigator window on the right in Xcode, select the Target and click on the Build Phases tab, click on the Link with Libraries drop down and click on the + button to open the file pop-up window. Select CoreLocation.framework from the list of available frameworks and click the Add button.
You’re going to build an application that will act as a compass, so you’re going to need an image of an arrow to act as the compass needle. Download or draw in the graphics package of your choice, an image of an arrow pointing upwards on a transparent background. Save or convert it to, a PNG format file. Drag-and-drop this into the 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.
Click on CompassViewController.xib file to open it in
Interface Builder. Drag and drop a UIImageView
from the Object Library into the
View, positioning it roughly in the center of your window, resizing the
bounding box to be a square, as in Figure 5-3. In the Attributes
inspector of the Utilities pane set the View mode to be “Aspect Fit”,
uncheck the “Opaque” checkbox in the Drawing section, and select the arrow
image that you added to your project in the Image drop down.
Next drag-and-drop four UILabel
elements from the Object Library into the View, position the four labels
as in Figure 5-3, and
change the text in the left most two to read “Magnetic Heading:” and “True
Heading:”.
Close the Utility pane and switch from the Standard to the Assistant
Editor. Control-Click and drag from the two right most UILabel
elements to the assistant editor to
create a magneticHeadingLabel
and
trueHeadingLabel
outlet, and then again
for the UIImageView
to create an
arrowImage
outlet, see Figure 5-3.
Then click on the CompassViewController.h interface file and go
ahead and declare the class as a CLLocationManagerDelegate
, remembering to import
the CoreLocation.h header file. After
doing so the interface should look like this:
#import <UIKit/UIKit.h> #import <CoreLocation/CoreLocation.h> @interface CompassViewController : UIViewController <CLLocationManagerDelegate> { IBOutlet UIImageView *arrowImage; IBOutlet UILabel *magneticHeadingLabel; IBOutlet UILabel *trueHeadingLabel; } @end
Save your changes, and click on the corresponding CompassViewController.m implementation file.
Uncomment the viewDidLoad
method and
the following code to the implementation. This will create an instance of
the CLLocationManager
class, and will
send both location and heading update messages to the designated delegate
class:
- (void)viewDidLoad { [super viewDidLoad]; CLLocationManager *locationManager = [[CLLocationManager alloc] init]; locationManager.delegate = self; if( [CLLocationManager locationServicesEnabled] && [CLLocationManager headingAvailable]) { [locationManager startUpdatingLocation]; [locationManager startUpdatingHeading]; } else { NSLog(@"Can't report heading"); } }
You can (optionally) filter the heading update messages using an angular filter. Changes in heading of less than this amount will not generate an update message to the delegate, for example:
locationManager.headingFilter = 5; // 5 degrees
The default value of this property is kCLHeadingFilterNone
. You should use this value
if you want to be notified of all heading updates. In this example, leave
the filter set to the default value. However if you want to filter
messages from Core Location this way, add the above line to your viewDidLoad
method inside the if-block:
if( [CLLocationManager locationServicesEnabled] &&
[CLLocationManager headingAvailable]) {
[locationManager startUpdatingLocation];
[locationManager startUpdatingHeading];
locationManager.headingFilter = 5; // 5 degrees
} else {
... code ...
}
The CLLocationManagerDelegate
protocol calls the locationManager:didUpdateHeading:
delegate
method when the heading is updated. You’re going to use this method to
update the user interface. Add the following code to your view
controller:
- (void)locationManager:(CLLocationManager*)manager didUpdateHeading:(CLHeading*)newHeading { if (newHeading.headingAccuracy > 0) { float magneticHeading = newHeading.magneticHeading; float trueHeading = newHeading.trueHeading; magneticHeadingLabel.text = [NSString stringWithFormat:@"%f", magneticHeading]; trueHeadingLabel.text = [NSString stringWithFormat:@"%f", trueHeading]; float heading = −1.0f * M_PI * newHeading.magneticHeading / 180.0f; arrowImage.transform = CGAffineTransformMakeRotation(heading); } }
If location updates are also enabled, the location manager returns both true heading and magnetic heading values. If location updates are not enabled, or the location of the device is not yet known, the location manager returns only the magnetic heading value and the value returned by this call will be −1.
Save your changes, then click on the Run button in the Xcode toolbar to deploy your new application to your device. If you hold the device in “Face Up” or “Portrait” mode you should see something very similar to Figure 5-4 below.
As it stands our application has a critical flaw. If the user orientates the device into Landscape Mode, the reported headings will be incorrect, or at least look incorrect to the user.
The magnetic and true headings are correct when the iPhone device is held like a traditional compass, in portrait mode, if the user rotates the device, the heading readings will still be in the original frame of reference. Even though the user has not changed the direction they are facing the heading values reported by the device will have changed. You’re going to have to correct for orientation before reporting headings back to the user, see Figure 5-5.
In the Project navigator, click on the CompassViewController.xib file to open it in
Interface Builder, then drag-and-drop another UILabel
from the Object Library in the Utility
pane into the View window. Use the Assistant Editor connect the label to
a new outlet in the CompassViewController.h interface file, as in
Figure 5-6.
After doing so, the interface file should look as below:
@interface CompassViewController :
UIViewController <CLLocationManagerDelegate> {
IBOutlet UILabel *trueHeadingLabel;
IBOutlet UILabel *magneticHeadingLabel;
IBOutlet UILabel *orientationLabel;
IBOutlet UIImageView *arrowImage;
}
We’re going to use this to report the current device orientation as we did in the Accelerometer application in Chapter 4.
Close the Assistant Editor and reopen the CompassViewController.h interface file in the Standard Editor. Go ahead and add the following convenience methods to the class definition:
- (float)magneticHeading:(float)heading fromOrientation:(UIDeviceOrientation) orientation; - (float)trueHeading:(float)heading fromOrientation:(UIDeviceOrientation) orientation; - (NSString *)stringFromOrientation:(UIDeviceOrientation) orientation;
Save your changes, and open the CompassViewController.m implementation file.
Since the CLHeading
object is read
only and you can’t modify it directly, you’l need to add the following
method to correct the magnetic heading for the device
orientation:
- (float)magneticHeading:(float)heading fromOrientation:(UIDeviceOrientation) orientation { float realHeading = heading; switch (orientation) { case UIDeviceOrientationPortrait: break; case UIDeviceOrientationPortraitUpsideDown: realHeading = realHeading + 180.0f; break; case UIDeviceOrientationLandscapeLeft: realHeading = realHeading + 90.0f; break; case UIDeviceOrientationLandscapeRight: realHeading = realHeading - 90.0f; break; default: break; } while ( realHeading > 360.0f ) { realHeading = realHeading - 360; } return realHeading; }
You will also need to add a corresponding method to correct the true heading:
- (float)trueHeading:(float)heading fromOrientation:(UIDeviceOrientation) orientation { float realHeading = heading; switch (orientation) { case UIDeviceOrientationPortrait: break; case UIDeviceOrientationPortraitUpsideDown: realHeading = realHeading + 180.0f; break; case UIDeviceOrientationLandscapeLeft: realHeading = realHeading + 90.0f; break; case UIDeviceOrientationLandscapeRight: realHeading = realHeading - 90.0f; break; default: break; } while ( realHeading > 360.0f ) { realHeading = realHeading - 360; } return realHeading; }
Finally, add the stringFromOrientation:
method from the
previous section Chapter 5. We’ll use
this to update the orientationLabel
outlet:
- (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; }
Return to the locationManager:didUpdateHeading:
delegate
method and modify the lines highlighted below to use the new methods and
update the headings depending on the device orientation:
- (void)locationManager:(CLLocationManager*)manager didUpdateHeading:(CLHeading*)newHeading { UIDevice *device = [UIDevice currentDevice]; orientationLabel.text = [self stringFromOrientation:device.orientation]; if (newHeading.headingAccuracy > 0) { float magneticHeading = [self magneticHeading:newHeading.magneticHeading fromOrientation:device.orientation]; float trueHeading = [self trueHeading:newHeading.trueHeading fromOrientation:device.orientation]; magneticHeadingLabel.text = [NSString stringWithFormat:@"%f", magneticHeading]; trueHeadingLabel.text = [NSString stringWithFormat:@"%f", trueHeading]; float heading = −1.0f * M_PI * newHeading.magneticHeading / 180.0f; arrowImage.transform = CGAffineTransformMakeRotation(heading); } }
Make sure you’ve saved all the changes to the implementation file and click on the Run button in the Xcode toolbar to deploy the application onto the device. If all goes well you should see the same compass display as before. However this time if you rotate the display, sees Figure 5-7, the heading values should be the same irrespective of the device orientation.
Although I have not discussed or implemented it here, if the
CLLocationManager
object encounters
an error, it will call the locationManager:didFailWithError:
delegate
method:
- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error { if ([error code] == kCLErrorDenied) { // User has denied the application's request to use location services. [manager stopUpdatingHeading]; } else if ([error code] == kCLErrorHeadingFailure) { // Heading could not be determined } }
To use the device to measure a magnetic field—for instance that of a small bar magnet, or the field generated by an electric current in a wire—you should first make a zero-point measurement of the ambient magnetic field of the Earth. Further readings should subtract this zero-point measurement.
When measuring, move the magnet to the device rather than moving the device to the magnet. Moving the device will cause the magnetic field of the Earth across the measuring sensor to change, which will spoil the zero point calibration you took earlier. If the device must be moved, only small movements should be attempted.
You can retrieve the raw magnetic field measurements along the X, Y
and Z-axes by querying the CLHeading
object passed to the locationManager:didUpdateHeading:
method:
- (void)locationManager:(CLLocationManager *)manager
didUpdateHeading:(CLHeading *)heading {
double x = heading.x;
double y = heading.y;
double z = heading.z;
double magnitude = sqrt(x*x + y*y + z*z);
... code ...
}
The values returned are normalized into a ±128 range, measured in microtesla (µT), representing the offset from the magnetic field lines measured by the magnetometer.
Apple provides sample code that displays the raw x, y, and z magnetometer values, a plotted history of those values, and a computed magnitude (size or strength) of the magnetic field. The code can be downloaded from the iPhone developer website at http://developer.apple.com/library/ios/#samplecode/Teslameter.