Correct CMMotionManager yaw for current UIInterfaceOrientation

3 min read 07-10-2024
Correct CMMotionManager yaw for current UIInterfaceOrientation


Navigating the Compass: Correcting Yaw for Screen Orientation in iOS

Problem: When working with CoreMotion's CMMotionManager in iOS apps, you might encounter inconsistencies in the yaw angle (rotation around the vertical axis) when the device's screen orientation changes. This can lead to inaccurate compass readings and a frustrating user experience.

Rephrased: Imagine you're using a compass app on your iPhone. If you rotate the phone from portrait to landscape, the compass needle might jump to a different direction even though you're pointing it in the same direction. This is because the compass data is based on the device's physical orientation, while the app's UI is based on the screen's orientation.

Scenario & Original Code:

Let's say we have a simple app that displays a compass needle using the CMMotionManager's deviceMotion property:

import CoreMotion

class CompassViewController: UIViewController {

    let motionManager = CMMotionManager()
    var compassView: UIImageView!

    override func viewDidLoad() {
        super.viewDidLoad()

        compassView = UIImageView(image: UIImage(named: "compass"))
        compassView.center = view.center
        view.addSubview(compassView)

        startMotionUpdates()
    }

    func startMotionUpdates() {
        if motionManager.isDeviceMotionAvailable {
            motionManager.deviceMotionUpdateInterval = 0.1
            motionManager.startDeviceMotionUpdates(to: OperationQueue.current!) { (motion, error) in
                if let motion = motion {
                    let yaw = motion.attitude.yaw
                    // ... Update compassView rotation based on yaw ...
                }
            }
        }
    }
}

This code will update the compass needle's rotation based on the yaw property of the CMAttitude. However, the yaw value is relative to the device's physical orientation, which might not match the screen's orientation.

Analysis & Solution:

To solve this, we need to adjust the yaw value based on the current UIInterfaceOrientation. Here's how:

  1. Get the current screen orientation:

    let currentOrientation = UIApplication.shared.windows.first?.windowScene?.interfaceOrientation ?? .portrait
    
  2. Adjust the yaw value based on the orientation:

    var adjustedYaw = yaw
    switch currentOrientation {
    case .landscapeLeft:
        adjustedYaw -= CGFloat.pi / 2
    case .landscapeRight:
        adjustedYaw += CGFloat.pi / 2
    case .portraitUpsideDown:
        adjustedYaw += CGFloat.pi
    default:
        break
    }
    
  3. Use the adjusted yaw to update the compass needle:

    compassView.transform = CGAffineTransform(rotationAngle: adjustedYaw)
    

Complete Code:

import CoreMotion
import UIKit

class CompassViewController: UIViewController {

    let motionManager = CMMotionManager()
    var compassView: UIImageView!

    override func viewDidLoad() {
        super.viewDidLoad()

        compassView = UIImageView(image: UIImage(named: "compass"))
        compassView.center = view.center
        view.addSubview(compassView)

        startMotionUpdates()
    }

    func startMotionUpdates() {
        if motionManager.isDeviceMotionAvailable {
            motionManager.deviceMotionUpdateInterval = 0.1
            motionManager.startDeviceMotionUpdates(to: OperationQueue.current!) { (motion, error) in
                if let motion = motion {
                    let yaw = motion.attitude.yaw
                    // Adjust yaw based on screen orientation
                    var adjustedYaw = yaw
                    let currentOrientation = UIApplication.shared.windows.first?.windowScene?.interfaceOrientation ?? .portrait
                    switch currentOrientation {
                    case .landscapeLeft:
                        adjustedYaw -= CGFloat.pi / 2
                    case .landscapeRight:
                        adjustedYaw += CGFloat.pi / 2
                    case .portraitUpsideDown:
                        adjustedYaw += CGFloat.pi
                    default:
                        break
                    }
                    // Update compassView rotation using the adjusted yaw
                    self.compassView.transform = CGAffineTransform(rotationAngle: adjustedYaw)
                }
            }
        }
    }
}

Additional Value:

  • Understanding the CMAttitude: The CMAttitude object represents the device's orientation in 3D space. It provides properties for roll, pitch, and yaw, each representing rotation around a specific axis.
  • Orientation Changes: You might need to handle cases where the screen orientation changes while the app is running. You can use NotificationCenter to listen for UIDeviceOrientationDidChangeNotification and update the compass accordingly.

References & Resources:

By understanding the relationship between device orientation and screen orientation, you can accurately display compass readings in your iOS apps, regardless of how the user holds their device.