How to Build an Accelerometer App in SwiftUI
Wrap CMMotionManager in an @Observable class, call startAccelerometerUpdates(to: .main) in onAppear, and read data.acceleration.x/y/z in the closure — SwiftUI re-renders automatically on every update.
import CoreMotion
import SwiftUI
@Observable
final class MotionManager {
var x: Double = 0
var y: Double = 0
var z: Double = 0
private let cm = CMMotionManager()
func start() {
cm.accelerometerUpdateInterval = 0.05 // 20 Hz
cm.startAccelerometerUpdates(to: .main) { [weak self] data, _ in
guard let self, let data else { return }
self.x = data.acceleration.x
self.y = data.acceleration.y
self.z = data.acceleration.z
}
}
func stop() { cm.stopAccelerometerUpdates() }
}
Full implementation
The complete example separates concerns into a MotionManager observable and a reusable AxisRow view. Each axis gets an animated progress bar calibrated to ±1g, and the scalar magnitude is shown prominently in the centre with a contentTransition(.numericText()) animation. ContentUnavailableView handles the simulator gracefully where no hardware is present.
import SwiftUI
import CoreMotion
// MARK: - MotionManager
@Observable
final class MotionManager {
var acceleration: CMAcceleration = CMAcceleration(x: 0, y: 0, z: 0)
var isActive = false
private let manager = CMMotionManager()
var isAvailable: Bool { manager.isAccelerometerAvailable }
func start() {
guard isAvailable, !isActive else { return }
manager.accelerometerUpdateInterval = 0.05 // 20 Hz
manager.startAccelerometerUpdates(to: .main) { [weak self] data, error in
guard let self, let data, error == nil else { return }
self.acceleration = data.acceleration
self.isActive = true
}
}
func stop() {
manager.stopAccelerometerUpdates()
isActive = false
}
}
// MARK: - AxisRow
struct AxisRow: View {
let label: String
let value: Double
let color: Color
var body: some View {
VStack(alignment: .leading, spacing: 6) {
HStack {
Text(label)
.font(.headline).foregroundStyle(color)
Spacer()
Text(String(format: "%+.3f g", value))
.font(.system(.body, design: .monospaced))
.accessibilityLabel("\(label) axis: \(String(format: "%.2f", value)) g")
}
GeometryReader { geo in
ZStack(alignment: .leading) {
Capsule().fill(Color.secondary.opacity(0.15)).frame(height: 10)
Capsule()
.fill(color)
.frame(width: geo.size.width * min(abs(value), 1.0), height: 10)
.animation(.easeOut(duration: 0.05), value: value)
}
}
.frame(height: 10)
}
}
}
// MARK: - AccelerometerView
struct AccelerometerView: View {
@State private var motion = MotionManager()
private var magnitude: Double {
let a = motion.acceleration
return sqrt(a.x * a.x + a.y * a.y + a.z * a.z)
}
var body: some View {
NavigationStack {
ScrollView {
VStack(spacing: 36) {
if motion.isAvailable {
VStack(spacing: 20) {
AxisRow(label: "X", value: motion.acceleration.x, color: .red)
AxisRow(label: "Y", value: motion.acceleration.y, color: .green)
AxisRow(label: "Z", value: motion.acceleration.z, color: .blue)
}
.padding(.horizontal)
VStack(spacing: 4) {
Text("Magnitude")
.font(.caption).foregroundStyle(.secondary)
Text(String(format: "%.3f g", magnitude))
.font(.system(size: 52, weight: .bold, design: .monospaced))
.contentTransition(.numericText())
.animation(.easeOut(duration: 0.05), value: magnitude)
.accessibilityLabel("Total magnitude: \(String(format: "%.2f", magnitude)) g")
}
} else {
ContentUnavailableView(
"Accelerometer Unavailable",
systemImage: "exclamationmark.triangle.fill",
description: Text("Run on a physical iPhone or iPad.")
)
}
}
.padding(.vertical, 24)
}
.navigationTitle("Accelerometer")
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button(motion.isActive ? "Stop" : "Start") {
motion.isActive ? motion.stop() : motion.start()
}
.buttonStyle(.borderedProminent)
.accessibilityLabel(motion.isActive ? "Stop accelerometer" : "Start accelerometer")
}
}
}
.onAppear { motion.start() }
.onDisappear { motion.stop() }
}
}
#Preview {
AccelerometerView()
}
How it works
-
1
@Observable MotionManager — The
@Observablemacro (iOS 17+) makes any stored property automatically tracked by SwiftUI. No@Publishedor manualobjectWillChange.send()needed. The singleCMMotionManagerinstance lives for the lifetime of the class — never create it inside aViewbody. -
2
startAccelerometerUpdates(to: .main) — Delivering updates on
OperationQueue.mainensures property writes happen on the main actor, which is where SwiftUI reads them. The 0.05 s interval (20 Hz) gives smooth animations without excessive CPU wake-ups. -
3
GeometryReader bar gauge —
AxisRowusesGeometryReaderto compute the bar width as a fraction of available space, clamped withmin(abs(value), 1.0)so values beyond ±1g don't overflow. A 0.05 s.easeOutanimation matches the sensor interval. -
4
contentTransition(.numericText()) — Applied to the magnitude
Text, this iOS 17+ transition animates digit-by-digit rather than cross-fading the whole label, giving a natural "counter" feel that degrades gracefully at high update rates. -
5
Lifecycle with onAppear / onDisappear — Starting in
onAppearand stopping inonDisappearties sensor activity to view visibility, preventing battery drain when the user navigates away or backgrounds the app.
Variants
Gravity-corrected user acceleration with CMDeviceMotion
Raw accelerometer data includes the ~1g gravitational component. Use CMDeviceMotion to get userAcceleration (gravity subtracted) and the gravity vector separately — useful for step counting or tilt-compensated gestures.
@Observable
final class DeviceMotionManager {
var userAcceleration: CMAcceleration = CMAcceleration(x: 0, y: 0, z: 0)
var gravity: CMAcceleration = CMAcceleration(x: 0, y: 0, z: 0)
private let manager = CMMotionManager()
func start() {
guard manager.isDeviceMotionAvailable else { return }
manager.deviceMotionUpdateInterval = 0.05
// .xArbitraryZVertical keeps Z aligned with world gravity
manager.startDeviceMotionUpdates(
using: .xArbitraryZVertical,
to: .main
) { [weak self] data, _ in
guard let self, let data else { return }
self.userAcceleration = data.userAcceleration // gravity removed
self.gravity = data.gravity // unit gravity vector
}
}
func stop() { manager.stopDeviceMotionUpdates() }
}
Shake detection without CMMotionManager
For a simple shake gesture, SwiftUI (backed by UIKit's responder chain) already provides motionBegan(_:with:). Bridge it with a UIViewRepresentable shake-receiving view or detect it by thresholding userAcceleration magnitude above ~2.5g in the DeviceMotion handler — no additional setup needed, and it works with standard accessibility events.
Common pitfalls
-
⚠
Simulator has no accelerometer.
isAccelerometerAvailablereturnsfalsein the simulator. Always guard on that flag and show aContentUnavailableView— otherwise your UI appears broken during development. Run on a real device for any meaningful testing. -
⚠
Don't store CMMotionManager inside a View struct. SwiftUI can recreate view values many times per second. If you create
CMMotionManager()directly in aView, each re-render starts a new manager and the old one leaks. Always store it in an@Observableclass or@StateObject. -
⚠
High update rates drain battery fast. 0.01 s (100 Hz) is the hardware maximum and appropriate for games; for UI purposes 0.05–0.1 s (10–20 Hz) is indistinguishable to the eye. Also add
accessibilityLabelto dynamic numeric values — VoiceOver will otherwise read every decimal change mid-sentence.
Prompt this with Claude Code
When using Soarias or Claude Code directly to implement this:
Implement an accelerometer app in SwiftUI for iOS 17+. Use CoreMotion — specifically CMMotionManager with a 20 Hz update interval. Show live x, y, z values as animated bar gauges and display total magnitude. Make it accessible (VoiceOver labels on each axis and magnitude). Stop updates in onDisappear and handle the simulator gracefully with ContentUnavailableView. Add a #Preview with realistic sample data.
Drop this prompt directly into the Build phase in Soarias — it will scaffold the MotionManager observable and the SwiftUI views in one shot, leaving you to iterate on the visual design in subsequent turns.
Related
FAQ
Does this work on iOS 16?
The CMMotionManager API itself is available back to iOS 4, but the @Observable macro requires iOS 17+. For iOS 16 compatibility, replace @Observable with ObservableObject, mark each property @Published, and use @StateObject in the view. Also swap ContentUnavailableView for a plain VStack fallback.
What is the difference between raw accelerometer data and CMDeviceMotion?
Raw CMAccelerometerData includes gravity (~1g pulling down), so a flat stationary phone still reads approximately (0, 0, −1). CMDeviceMotion uses sensor fusion (accelerometer + gyroscope) to separate gravity from userAcceleration, which measures only the motion you apply. Use raw data for tilt detection; use DeviceMotion for gesture recognition and step counting.
What is the UIKit equivalent?
UIKit offers motionBegan(_:with:) / motionEnded(_:with:) on UIResponder for basic shake events, but for continuous accelerometer data the API is identical — you use the same CMMotionManager in a UIViewController and call the same startAccelerometerUpdates method. CoreMotion is framework-agnostic and works equally in UIKit and SwiftUI.
Last reviewed: 2026-05-12 by the Soarias team.