Search code examples
iosswiftswiftuicore-motion

SwiftUI device motion stutters after some time


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.


Solution

  • According to 'the docs' Because the processed events might arrive at a high rate, using the main operation queue is not recommended