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?
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
MyViewcan be updated to use this struct and class.Change the declaration of
referenceAttitudeto:Then replace
applyRotationEffectwith: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
MyViewclass with the changes from this answer. Then replace the stockViewControllerimplementation with the following:Run the app and rotate away.