I'm trying to use the CoreMotion
framework in a SwiftUI iOS app to move an object on the screen (similar idea to the level feature in the Measure app).
I have a MotionManager
class to handle the device motion updates:
class MotionManager: ObservableObject {
private var motionManager: CMMotionManager
private var pitch: Double = 0
private var roll: Double = 0
private var yaw: Double = 0
@Published
var x: Double = 0
@Published
var y: Double = 0
init() {
self.motionManager = CMMotionManager()
self.motionManager.deviceMotionUpdateInterval = 1 / 60
self.motionManager.startDeviceMotionUpdates(to: .main) { (deviceMotionData, error) in
guard error == nil else {
print(error!)
return
}
if let attitudeData = deviceMotionData?.attitude {
self.pitch = attitudeData.pitch
self.roll = attitudeData.roll
self.yaw = attitudeData.yaw
}
self.updatePositions()
}
}
private func updatePositions() {
let rollDegrees = self.roll * 180 / .pi
let pitchDegrees = self.pitch * 180 / .pi
self.x = convertAngleToRange(angle: rollDegrees, direction: .x)
self.y = convertAngleToRange(angle: pitchDegrees, direction: .y)
}
}
The convertAngleToRange
method is defined elsewhere but it essentially maps a pitch/roll value from radians to a pixel location.
I have my main view:
struct ContentView: View {
@StateObject
var motion: MotionManager = MotionManager()
var body: some View {
NavigationView {
NavigationLink(destination: GuideView()) {
EmptyView()
}
}
.environmentObject(motion)
}
}
Then, the GuideView
view:
struct GuideView: View {
@EnvironmentObject
var motion: MotionManager
var body: some View {
GeometryReader { geometry in
Representation(x: $motion.x, y: $motion.y)
.frame(width: geometry.size.width, height: geometry.size.height)
}
.statusBar(hidden: true)
.navigationBarHidden(true)
}
}
Finally, the Representation
view which contains the circle that moves:
struct Representation: View {
@Binding
var x: Double
@Binding
var y: Double
var body: some View {
GeometryReader { geometry in
Circle()
.fill(Color.gray)
.frame(width: geometry.size.width * 0.5)
.position(
x: geometry.size.width * 0.5 * (CGFloat(x) + 1),
y: geometry.size.height * 0.5 + geometry.size.width * 0.5 * CGFloat(y)
)
}
}
}
This works fine initially, but after some time the circle's movement becomes laggy and seems to stutter. If I move the device around and then set it down flat on a table, it takes several seconds to catch up to itself and become still.
My thinking is that the publishing of the motion.x
and motion.y
values combined with refreshing the display are causing a buildup of events which is why it becomes very laggy. I've tried reducing the motionManager.deviceMotionUpdateInterval
in the MotionManager
class but even at lower values it eventually starts to lag. I wonder if there is too much being done each time the device motion updates, but I can't seem to figure out how to reduce this because I still need to process the raw pitch and roll values in order to convert them to coordinates.
I could use some tips on how to avoid this issue or at least help diagnose where the issue is.
According to 'the docs' Because the processed events might arrive at a high rate, using the main operation queue is not recommended