How is it possible to detect the rotation of an iPhone that lies on a table like the compass but is showing a more accurate rotation? I tried to use the compass with the magnetic heading of the iPhone but it appears to be quite unreliable and jumps unexpectedly. The gyroscope can be used but the original reference point drifts with the gyroscope over time. This example combines the compass and the gyroscope using the compass as a reference as long as it is stable and using the fast update of the gyroscope between the times the compass is unstable.

In the app below there are 3 rotating graphics:

  • Yellow is magnetic heading.
  • Blue is a compass offset (always following the magnetic heading with a decided offset)
  • Black is the gyroscope (reset every time the compass is stable)

The application uses the CLLocationManager to access the magnetic heading of the compass and CMMotionManager to access the gyroscope. The values I use are newHeading.magneticHeading and motion.attitude.yaw. The magnetic heading gives a value of 360 degrees. The yaw value gives a value between -180 and 180.

Compass from Location Manager

First we initialize the location manager. This will only work on the device and not in the simulator.

locationManager=[[CLLocationManager alloc] init];
locationManager.desiredAccuracy = kCLLocationAccuracyBest;
locationManager.delegate=self;
       
if([CLLocationManager headingAvailable] == YES){
        NSLog(@"Heading is available");
} else {
        NSLog(@"Heading isn’t available");
}
[locationManager startUpdatingHeading];

As shown in the code above we delegate the listener to RotationViewController. The following code is needed to listen to updates for the compass:

#import <UIKit/UIKit.h>
#import <math.h>
#import <CoreMotion/CoreMotion.h> // For the gyroscope
#import <CoreLocation/CoreLocation.h> // For the compass

@interface RotationViewController : UIViewController <CLLocationManagerDelegate> {

Yaw data from gyroscope

Next step is to listen to updates from the gyroscope. We do that by listening to motionManager’s CMAttitude updates. We use the yaw which is retrieved in radians and we convert it to degrees.

motionManager = [[CMMotionManageralloc]  init];
 motionManager.deviceMotionUpdateInterval = 1.0/60.0;
 opQ = [[NSOperationQueuecurrentQueue] retain];
   
 if(motionManager.isDeviceMotionAvailable) {
    motionHandler = ^ (CMDeviceMotion *motion, NSError *error) {
        CMAttitude *currentAttitude = motion.attitude;
        float yawValue = currentAttitude.yaw;
        float yawDegrees = CC_RADIANS_TO_DEGREES(yawValue);
    };
} else {

    [motionManager release];
}

[motionManagerstartDeviceMotionUpdatesToQueue:opQwithHandler:motionHandler];

We now have the compass and the gyroscope. In this example I wanted to offset the magnetic heading so it always points at a certain direction. I decide on that direction when I press the “Calibrate”-button I set my offset from the magnetic heading. updatedHeading is the latest magnetic heading I got from the locationManager. northOffset becomes my reference to where I want the gyroscope to always origin from.

- (IBAction)calibrate:(id)sender
{  
    northOffest = updatedHeading - 0;
}

Compensating for compass inaccuracies

Now that we have the northOffset we want to use it together with the gyroscope. Since the compass is jumping sometimes we want to only use the compass value when it is stable. A timer is created with the updater method that checks if the value of the magnetic heading has changed. The interval is called every other second. If the magnetic heading hasn’t changed from last time it is considered a stable value. The stable value is added to newCompassTarget which is use for the gyroscope to get a new reference.

- (void)updater:(NSTimer *)timer
{
    // Om inte compassen rört sig på ett tag kalibrera gyron efter det
    if(updatedHeading == oldHeading) {
       NSLog(@"Update gyro");
       newCompassTarget = (0 - updatedHeading) + northOffest;
       offsetG = currentYaw;
        updateCompass = 1;
    } else {
        updateCompass = 0;
    }
   
    oldHeading = updatedHeading;
}

newCompassTarget is used in the code below so that the gyroscope always strive to go to the new reference of the compass but with the offset we use in the variable offsetG which is the difference between where the gyroscope was with the old and compared to the new heading.

motionHandler = ^ (CMDeviceMotion *motion, NSError *error) {
            CMAttitude *currentAttitude = motion.attitude;
            float yawValue = currentAttitude.yaw;
            float yawDegrees = CC_RADIANS_TO_DEGREES(yawValue);
            currentYaw = yawDegrees;
           
            yawDegrees = newCompassTarget + (yawDegrees - offsetG);
           
            if(yawDegrees < 0) {
                yawDegrees = yawDegrees + 360;
            }
       
            compassDif.text = [NSString stringWithFormat:@"Gyro: %f",yawDegrees];
           
            float gyroDegrees = (yawDegrees*radianConst);
           
            if(updateCompass) {
                [UIView beginAnimations:nil context:NULL];
                [UIView setAnimationDuration:0.25];
                [UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
                [rotateImg setTransform:CGAffineTransformMakeRotation(gyroDegrees)];
                [UIView commitAnimations];
               
                updateCompass = 0;

            } else {
                rotateImg.transform = CGAffineTransformMakeRotation(gyroDegrees);
            }
        };

Download project source code here.