skip to Main Content

I am implementing a custom UIView that listens to the device attitude in order to transform the view in 3d.

The following implementation works well:

class MyView: UIView {
    
    private let motionManager = CMMotionManager()
    private var referenceAttitude: CMAttitude?
    private let maxRotation = 45.0 * .pi / 180.0
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        commonInit()
    }

    private func commonInit() {
        motionManager.deviceMotionUpdateInterval = 0.02
        motionManager.startDeviceMotionUpdates(to: .main) { [weak self] (data, error) in
            guard let deviceMotion = data, error == nil else {
                return
            }
            self?.applyRotationEffect(deviceMotion)
        }
    }

    private func applyRotationEffect(_ deviceMotion: CMDeviceMotion) {
        guard let referenceAttitude = self.referenceAttitude else {
            self.referenceAttitude = deviceMotion.attitude
            return
        }

        deviceMotion.attitude.multiply(byInverseOf: referenceAttitude)
        
        let clampedPitch = min(max(deviceMotion.attitude.pitch, -maxRotation), maxRotation)
        let clampedRoll = min(max(deviceMotion.attitude.roll, -maxRotation), maxRotation)
        let clampedYaw = min(max(deviceMotion.attitude.yaw, -maxRotation), maxRotation)
        
        var transform = CATransform3DIdentity
        transform.m34 = 1 / 500
        transform = CATransform3DRotate(transform, CGFloat(clampedPitch), 1, 0, 0)
        transform = CATransform3DRotate(transform, CGFloat(clampedRoll), 0, 1, 0)
        transform = CATransform3DRotate(transform, CGFloat(clampedYaw), 0, 0, 1)
        
        layer.transform = transform
    }
}

I store a referenceAttitude to be able to know how much the device moved in a given direction. I also make it possible to rotate max 45 degrees in every direction.

I would like to be able to update the referenceAttitude when the phone moves too much in a given direction.

For example, if I rotate the phone 60 degrees on the x axis, I’d like to shift referenceAttitude by 60 – 45 = 15 degrees on this axis. By doing so, the user won’t have to move 15 degrees and more in the other direction to make the view move again.

I did not find a way to do that, any idea on how to implement that?

2

Answers


  1. Since there’s no API to create or modify instances of CMAttitude, the first step to is to create your own struct representing pitch, roll, and yaw.

    struct MyAttitude {
        let pitch: Double
        let roll: Double
        let yaw: Double
    
        init(attitude: CMAttitude) {
            self.pitch = attitude.pitch
            self.roll = attitude.roll
            self.yaw = attitude.yaw
        }
    
        init(pitch: Double, roll: Double, yaw: Double) {
            self.pitch = pitch
            self.roll = roll
            self.yaw = yaw
        }
    }
    

    Next, you can create a class that contains the logic needed to calculate adjusted attitudes from the current reference attitude and the newest device attitude. This class can update the reference attitude when the user starts rotating back in the opposite direction after rotating beyond the maximum angle.

    class ReferenceAttitude {
        var reference: MyAttitude
        var maxPitch = 0.0
        var minPitch = 0.0
        var maxRoll = 0.0
        var minRoll = 0.0
        var maxYaw = 0.0
        var minYaw = 0.0
    
        init(with attitude: CMAttitude) {
            reference = MyAttitude(pitch: attitude.pitch, roll: attitude.roll, yaw: attitude.yaw)
        }
    
        func relative(to attitude: CMAttitude, clampedTo angle: Double) -> MyAttitude {
            // Get the current relative attitude
            var pitch = attitude.pitch - reference.pitch
            var roll = attitude.roll - reference.roll
            var yaw = attitude.yaw - reference.yaw
    
            // Adjust the pitch as needed
            var newPitch: Double?
            // Is the pitch greater than the max angle?
            if (pitch > angle) {
                // Yes, are we still increasing?
                if (pitch >= maxPitch) {
                    // Yes, track how far we've pitched so far
                    maxPitch = pitch
                } else {
                    // No, we maxed out and are now decreasing. Readjust to a new pitch reference
                    newPitch = attitude.pitch - angle
                    maxPitch = angle
                    minPitch = 0
                }
                // Clamp the pitch
                pitch = angle
            // Is the pitch less than the min angle?
            } else if (pitch < -angle) {
                // Yes, are we still decreasing?
                if (pitch <= minPitch) {
                    // Yes, track how far we've pitched so far
                    minPitch = pitch
                } else {
                    // No, we mined out and are now increasing. Readjust to a new pitch reference
                    newPitch = attitude.pitch + angle
                    minPitch = -angle
                    maxPitch = 0
                }
                // Clamp the pitch
                pitch = -angle
            }
    
            // Same logic for roll as pitch
            var newRoll: Double?
            if (roll > angle) {
                if (roll >= maxRoll) {
                    maxRoll = roll
                } else {
                    newRoll = attitude.roll - angle
                    maxRoll = angle
                    minRoll = 0
                }
                roll = angle
            } else if (roll < -angle) {
                if (roll <= minRoll) {
                    minRoll = roll
                } else {
                    newRoll = attitude.roll + angle
                    minRoll = -angle
                    maxRoll = 0
                }
                roll = -angle
            }
    
            // Same logic for yaw as pitch
            var newYaw: Double?
            if (yaw > angle) {
                if (yaw >= maxYaw) {
                    maxYaw = yaw
                } else {
                    newYaw = attitude.yaw - angle
                    maxYaw = angle
                    minYaw = 0
                }
                yaw = angle
            } else if (yaw < -angle) {
                if (yaw <= minYaw) {
                    minYaw = yaw
                } else {
                    newYaw = attitude.yaw + angle
                    minYaw = -angle
                    maxYaw = 0
                }
                yaw = -angle
            }
    
            // Do we have any new reference values?
            if newPitch != nil || newRoll != nil || newYaw != nil {
                // Yes, create a new reference
                reference = MyAttitude(pitch: newPitch ?? reference.pitch, roll: newRoll ?? reference.roll, yaw: newYaw ?? reference.yaw)
            }
    
            // Return the new adjusted relative attitude
            return .init(pitch: pitch, roll: roll, yaw: yaw)
        }
    }
    

    Finally, your MyView can be updated to use this struct and class.

    Change the declaration of referenceAttitude to:

    private var referenceAttitude: ReferenceAttitude?
    

    Then replace applyRotationEffect with:

    private func applyRotationEffect(_ deviceMotion: CMDeviceMotion) {
        guard let referenceAttitude = self.referenceAttitude else {
            self.referenceAttitude = ReferenceAttitude(with: deviceMotion.attitude)
            return
        }
    
        let adjusted = referenceAttitude.relative(to: deviceMotion.attitude, clampedTo: maxRotation)
    
        var transform = CATransform3DIdentity
        transform.m34 = 1 / 500
        transform = CATransform3DRotate(transform, CGFloat(adjusted.pitch), 1, 0, 0)
        transform = CATransform3DRotate(transform, CGFloat(adjusted.roll), 0, 1, 0)
        transform = CATransform3DRotate(transform, CGFloat(adjusted.yaw), 0, 0, 1)
    
        layer.transform = transform
    }
    

    As an example, let’s say the user starts the app with the phone laying flat on table. While keeping the phone flat on the table, if the user starts rotating the phone clockwise, the yaw is being changed. If the phone is rotated up to 45º the view moves a corresponding amount. If the device keeps rotating clockwise, the view stays at its max rotation of 45º. But as soon as the device begins to rotate back counter-clockwise (anti-clockwise), the view begins to rotate back. No need to get back to 45º first for the view to start rotating again.


    To test this out, create a new Swift/Storyboard project. Add the new struct and class from this answer. Add the OP’s MyView class with the changes from this answer. Then replace the stock ViewController implementation with the following:

    class ViewController: UIViewController {
        var rotate = MyView()
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            view.backgroundColor = .black
            
            rotate.backgroundColor = .red
            rotate.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(rotate)
            NSLayoutConstraint.activate([
                rotate.centerXAnchor.constraint(equalTo: view.centerXAnchor),
                rotate.centerYAnchor.constraint(equalTo: view.centerYAnchor),
                rotate.heightAnchor.constraint(equalToConstant: 300),
                rotate.widthAnchor.constraint(equalToConstant: 200),
            ])
        }
    }
    

    Run the app and rotate away.

    Login or Signup to reply.
  2. To update the referenceAttitude when the device moves beyond the maximum rotation angle, you can introduce a threshold check in the applyRotationEffect method. If the device exceeds the maximum rotation angle in any direction, you can update the referenceAttitude accordingly.

    Here’s an updated version of the applyRotationEffect method that includes the threshold check and updates the referenceAttitude when necessary:

    private func applyRotationEffect(_ deviceMotion: CMDeviceMotion) {
        guard let referenceAttitude = self.referenceAttitude else {
            self.referenceAttitude = deviceMotion.attitude
            return
        }
    
        deviceMotion.attitude.multiply(byInverseOf: referenceAttitude)
    
        let clampedPitch = min(max(deviceMotion.attitude.pitch, -maxRotation), maxRotation)
        let clampedRoll = min(max(deviceMotion.attitude.roll, -maxRotation), maxRotation)
        let clampedYaw = min(max(deviceMotion.attitude.yaw, -maxRotation), maxRotation)
    
        // Check if any rotation exceeds the maximum rotation angle
        let shouldUpdateReferenceAttitude = abs(deviceMotion.attitude.pitch) > maxRotation ||
            abs(deviceMotion.attitude.roll) > maxRotation ||
            abs(deviceMotion.attitude.yaw) > maxRotation
    
        if shouldUpdateReferenceAttitude {
            // Calculate the excess rotation
            let excessPitch = abs(deviceMotion.attitude.pitch) - maxRotation
            let excessRoll = abs(deviceMotion.attitude.roll) - maxRotation
            let excessYaw = abs(deviceMotion.attitude.yaw) - maxRotation
    
            // Update the reference attitude by subtracting the excess rotation
            let updatedReferenceAttitude = CMAttitude()
            updatedReferenceAttitude.pitch = excessPitch >= 0 ? excessPitch.copysign(to: deviceMotion.attitude.pitch) : deviceMotion.attitude.pitch
            updatedReferenceAttitude.roll = excessRoll >= 0 ? excessRoll.copysign(to: deviceMotion.attitude.roll) : deviceMotion.attitude.roll
            updatedReferenceAttitude.yaw = excessYaw >= 0 ? excessYaw.copysign(to: deviceMotion.attitude.yaw) : deviceMotion.attitude.yaw
    
            // Update the reference attitude
            referenceAttitude.multiply(byInverseOf: updatedReferenceAttitude)
            self.referenceAttitude = referenceAttitude
        }
    
        var transform = CATransform3DIdentity
        transform.m34 = 1 / 500
        transform = CATransform3DRotate(transform, CGFloat(clampedPitch), 1, 0, 0)
        transform = CATransform3DRotate(transform, CGFloat(clampedRoll), 0, 1, 0)
        transform = CATransform3DRotate(transform, CGFloat(clampedYaw), 0, 0, 1)
    
        layer.transform = transform
    }
    

    In this updated implementation, when any of the rotation angles exceeds the maximum rotation (maxRotation), the excess rotation is calculated (excessPitch, excessRoll, excessYaw). Then, a new CMAttitude instance, updatedReferenceAttitude, is created to store the excess rotations.

    The signs of the excess rotations are determined using the copysign(to:) method to maintain the correct direction. Finally, the referenceAttitude is updated by multiplying it with the inverse of updatedReferenceAttitude, and the updated referenceAttitude is used for subsequent calculations.

    With this approach, the referenceAttitude will be adjusted when the device exceeds the maximum rotation angle, allowing the view to respond to further rotations without requiring the user to move the device in the opposite direction.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search