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
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.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.
Finally, your
MyView
can be updated to use this struct and class.Change the declaration of
referenceAttitude
to:Then replace
applyRotationEffect
with: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 stockViewController
implementation with the following:Run the app and rotate away.
To update the
referenceAttitude
when the device moves beyond the maximum rotation angle, you can introduce a threshold check in theapplyRotationEffect
method. If the device exceeds the maximum rotation angle in any direction, you can update thereferenceAttitude
accordingly.Here’s an updated version of the
applyRotationEffect
method that includes the threshold check and updates thereferenceAttitude
when necessary: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 newCMAttitude
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, thereferenceAttitude
is updated by multiplying it with the inverse ofupdatedReferenceAttitude
, and the updatedreferenceAttitude
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.