---
name: core-motion
description: "Core Motion on iOS and watchOS: accelerometer, gyroscope, magnetometer, fused device motion and reference frames, CMPedometer steps, distance and floors, activity recognition, barometric and absolute altitude, AirPods head tracking, batched high-rate workout capture, watchOS water submersion and depth. Use when building tilt controls, step counters, activity-aware features, altitude tracking or other motion interactions."
metadata:
  source: multi-agent-pipeline
---

# Core Motion

Core Motion turns the accelerometer, gyroscope, magnetometer and barometer into
data an app can act on. It fuses the raw sensors into a single device-motion
stream and also ships higher-level services: a pedometer, an activity
classifier and an altimeter. Fitness, navigation, games and accessibility
features all build on it. Examples assume Swift 6.3 and iOS 26 or later.

Longer SwiftUI services, batched sensors, head tracking, a step dashboard,
activity-driven modes and dive support live in the
[motion patterns reference](references/motion-patterns.md).

Covers: activity recognition (walking, running, driving, cycling) and
distance and floor counts from `CMPedometer`. Not for HealthKit workout
sessions or SensorKit research data.

## Setup

### Usage string

Info.plist must carry `NSMotionUsageDescription`, a sentence telling the user
why the app reads motion. If the key is absent, the process is terminated the first
time it touches a protected motion API.

### Authorization

Several managers expose an authorization status: `CMPedometer`,
`CMMotionActivityManager`, `CMAltimeter`, `CMHeadphoneMotionManager`,
`CMBatchedSensorManager` and `CMWaterSubmersionManager`. Read it before you
start updates. The raw `CMMotionManager` streams (accelerometer, gyroscope,
device motion) have no request call at all; ship the usage string anyway and
treat errors delivered to the handler as the failure signal.

```swift
import CoreMotion

enum MotionAccess {
    case ready, willPrompt, blocked
}

func pedometerAccess() -> MotionAccess {
    switch CMPedometer.authorizationStatus() {
    case .authorized: return .ready
    case .notDetermined: return .willPrompt      // system asks on first query
    case .denied, .restricted: return .blocked   // point the user at Settings
    @unknown default: return .blocked
    }
}
```

## CMMotionManager: raw sensors

Create one `CMMotionManager` for the whole app. Extra instances compete for
the same hardware and every one of them receives updates at a lower rate.

### Accelerometer

```swift
@MainActor let sensors = CMMotionManager()

@MainActor func beginShakeDetection(onShake: @escaping () -> Void) {
    guard sensors.isAccelerometerAvailable else { return }
    sensors.accelerometerUpdateInterval = 1.0 / 60.0
    sensors.startAccelerometerUpdates(to: .main) { sample, error in
        guard let a = sample?.acceleration, error == nil else { return }
        let magnitude = (a.x * a.x + a.y * a.y + a.z * a.z).squareRoot()
        if magnitude > 2.5 { onShake() }
    }
}

@MainActor func endShakeDetection() { sensors.stopAccelerometerUpdates() }
```

`CMMotionManager` is not `Sendable`, so Swift 6 rejects a plain global `let` of
it ("not concurrency-safe"). The shared instance is therefore `@MainActor`, and
so is every function that touches it; the same goes for the pedometer,
activity and altimeter globals below. Handlers queued on `.main` run on the
main thread either way.

### Gyroscope

```swift
@MainActor func beginSpinTracking() {
    guard sensors.isGyroAvailable else { return }
    sensors.gyroUpdateInterval = 1.0 / 30.0
    sensors.startGyroUpdates(to: .main) { sample, error in
        guard let spin = sample?.rotationRate, error == nil else { return }
        _ = spin.z   // radians per second around each axis
    }
}

@MainActor func endSpinTracking() { sensors.stopGyroUpdates() }
```

### Polling from a game loop

A game that already runs a per-frame loop can skip the handler. Call the
start method with no arguments and read the latest value when drawing.

```swift
sensors.startAccelerometerUpdates()

@MainActor func tick() {   // called from a display link or render loop
    guard let latest = sensors.accelerometerData else { return }
    applyTilt(x: latest.acceleration.x)
}
```

## Device motion

`CMDeviceMotion` blends accelerometer, gyroscope and magnetometer readings.
Each sample carries `attitude`, `userAcceleration` (gravity already removed),
`gravity`, `rotationRate`, a calibrated `magneticField` and `heading`, which is
reported in degrees relative to the active reference frame.

Never hard-code a reference frame. Ask
`CMMotionManager.availableAttitudeReferenceFrames()` at runtime and fall back
to `.xArbitraryZVertical`.

```swift
@MainActor func beginOrientationTracking() {
    guard sensors.isDeviceMotionAvailable else { return }
    let offered = CMMotionManager.availableAttitudeReferenceFrames()
    let frame: CMAttitudeReferenceFrame =
        offered.contains(.xArbitraryCorrectedZVertical)
        ? .xArbitraryCorrectedZVertical
        : .xArbitraryZVertical

    sensors.deviceMotionUpdateInterval = 1.0 / 30.0
    sensors.startDeviceMotionUpdates(using: frame, to: .main) { reading, failure in
        guard let reading, failure == nil else { return }
        let tilt = reading.attitude          // roll, pitch, yaw in radians
        let push = reading.userAcceleration  // in g, gravity removed
        let down = reading.gravity
        let degrees = reading.heading
        _ = (tilt.roll, push.z, down.y, degrees)
    }
}

@MainActor func endOrientationTracking() { sensors.stopDeviceMotionUpdates() }
```

### Reference frames

| Frame | X axis | Needs | Good for |
|---|---|---|---|
| `.xArbitraryZVertical` | wherever the device pointed at start; the default | nothing extra | most games, tilt |
| `.xArbitraryCorrectedZVertical` | same, with gyro drift corrected over time | nothing extra | longer tilt sessions |
| `.xMagneticNorthZVertical` | magnetic north | magnetometer | compass-like UI |
| `.xTrueNorthZVertical` | true north | magnetometer and location | geographic alignment |

For plain tilt, pick one of the two arbitrary frames: they do not depend on the
magnetometer or on location. Before asking for a north frame, confirm
`availableAttitudeReferenceFrames().contains(...)` includes it.

## CMPedometer

The pedometer reports step count, distance, pace, cadence and floors climbed
or descended.

```swift
@MainActor let stepCounter = CMPedometer()

@MainActor func loadTodaysWalk() {
    guard CMPedometer.isStepCountingAvailable() else { return }
    let midnight = Calendar.current.startOfDay(for: .now)
    stepCounter.queryPedometerData(from: midnight, to: .now) { summary, error in
        guard let summary, error == nil else { return }
        let steps = summary.numberOfSteps.intValue
        let meters = summary.distance?.doubleValue ?? 0
        let climbed = summary.floorsAscended?.intValue ?? 0
        _ = (steps, meters, climbed)
    }
}

@MainActor func followLiveSteps(since start: Date) {
    stepCounter.startUpdates(from: start) { summary, _ in
        _ = summary?.numberOfSteps
    }
}

@MainActor func stopLiveSteps() { stepCounter.stopUpdates() }
```

Each metric has its own capability check:

- `isStepCountingAvailable()` - step hardware present
- `isDistanceAvailable()` - distance estimates
- `isFloorCountingAvailable()` - floors, which need a barometer
- `isPaceAvailable()` - pace
- `isCadenceAvailable()` - cadence

## CMMotionActivityManager

The classifier reports stationary, walking, running, cycling and automotive
states.

```swift
@MainActor let classifier = CMMotionActivityManager()

@MainActor func watchMovement() {
    guard CMMotionActivityManager.isActivityAvailable() else { return }
    classifier.startActivityUpdates(to: .main) { activity in
        guard let activity else { return }
        let flags = (activity.walking, activity.running, activity.cycling,
                     activity.automotive, activity.stationary)
        let certainty = activity.confidence.rawValue   // 0 low, 1 medium, 2 high
        _ = (flags, certainty)
    }
}

@MainActor func stopWatchingMovement() { classifier.stopActivityUpdates() }
```

### Looking back

```swift
@MainActor func lastDayOfActivity() {
    let end = Date.now
    let start = end.addingTimeInterval(-24 * 60 * 60)
    classifier.queryActivityStarting(from: start, to: end, to: .main) { entries, _ in
        for entry in entries ?? [] { _ = entry.startDate }
    }
}
```

## CMAltimeter

The same `NSMotionUsageDescription` covers the altimeter. When the user denies
motion access, altitude simply stops being available and the handlers receive
errors.

### Relative altitude

```swift
@MainActor let barometer = CMAltimeter()

@MainActor func trackClimb() {
    guard CMAltimeter.isRelativeAltitudeAvailable() else { return }
    barometer.startRelativeAltitudeUpdates(to: .main) { reading, error in
        guard let reading, error == nil else { return }
        let gainedMeters = reading.relativeAltitude.doubleValue
        let kilopascals = reading.pressure.doubleValue
        _ = (gainedMeters, kilopascals)
    }
}

@MainActor func stopClimb() { barometer.stopRelativeAltitudeUpdates() }
```

### Absolute altitude

Absolute altitude is height above sea level estimated by the system, not a GPS
fix. Supported hardware starts at iPhone 12 on the phone side; on the wrist it
starts at Apple Watch Series 6 and Apple Watch SE. Always gate on `isAbsoluteAltitudeAvailable()`.

```swift
@MainActor func trackElevation() {
    guard CMAltimeter.isAbsoluteAltitudeAvailable() else { return }
    barometer.startAbsoluteAltitudeUpdates(to: .main) { reading, error in
        guard let reading, error == nil else { return }
        _ = (reading.altitude, reading.accuracy)   // both in meters
    }
}

@MainActor func stopElevation() { barometer.stopAbsoluteAltitudeUpdates() }
```

## Update rates and battery

| Use case | Frequency | Energy impact | Interval to set |
|---|---|---|---|
| UI that follows orientation | 10 Hz | low | `1.0 / 10.0` |
| Casual game | 30 Hz | moderate | `1.0 / 30.0` |
| Fast action game | 60 Hz | high | `1.0 / 60.0` |
| Fastest iPhone sampling | 100 Hz | very high | `1.0 / 100.0` |

Pick the slowest rate the feature tolerates. The ceiling differs from one
device model to the next, so no code path should rely on a fixed maximum. For high-frequency workout
analysis, prefer `CMBatchedSensorManager` where it is supported, and read its
`accelerometerDataFrequency` or `deviceMotionDataFrequency`. Those two
properties are read-only; there is nothing to assign.

## Common mistakes

- **Several `CMMotionManager` objects.** Rates drop for all of them. Wrap one
  manager in a shared `@Observable` service exposed as `static let shared`.
- **No availability guard.** Starting gyroscope updates without checking
  `isGyroAvailable` can crash on hardware that lacks one. Guard first and tell
  the user the feature is unsupported.
- **Never stopping.** Sensors keep running and drain the battery. Pair every
  start with a stop in the matching lifecycle callback, for example
  `viewDidAppear` with `viewDidDisappear`.
- **Oversampling.** A compass needle does not need 100 Hz; 10 Hz is plenty.
- **Treating activity flags as exclusive.** Several can be true together.
  Combine them with `confidence`, for example act on `walking` only at `.high`,
  and on `automotive` only when confidence is not `.low`.

## Review checklist

- [ ] Info.plist has a clear `NSMotionUsageDescription`
- [ ] The app owns exactly one shared `CMMotionManager`
- [ ] Availability (`isAccelerometerAvailable` and friends) is checked before any start call
- [ ] Authorization is read before pedometer and activity APIs are used
- [ ] The update interval is the lowest that still works
- [ ] Every `start*Updates` has a `stop*Updates` in the opposite lifecycle event
- [ ] Handlers run on a suitable queue and heavy processing stays off the main thread
- [ ] `CMMotionActivity.confidence` is checked before the app reacts
- [ ] Handler `error` parameters are inspected
- [ ] Device-motion code calls `availableAttitudeReferenceFrames()` before choosing a frame
- [ ] The frame matches the need; true north is never the default

## References

- [Motion patterns](references/motion-patterns.md) - SwiftUI services, batched sensors, headphone motion, pedometer dashboard, activity modes, water submersion
- [Core Motion](https://developer.apple.com/documentation/coremotion)
- [CMMotionManager](https://developer.apple.com/documentation/coremotion/cmmotionmanager)
- [CMPedometer](https://developer.apple.com/documentation/coremotion/cmpedometer)
- [CMMotionActivityManager](https://developer.apple.com/documentation/coremotion/cmmotionactivitymanager)
- [CMDeviceMotion](https://developer.apple.com/documentation/coremotion/cmdevicemotion)
- [CMAltimeter](https://developer.apple.com/documentation/coremotion/cmaltimeter)
- [CMAbsoluteAltitudeData](https://developer.apple.com/documentation/coremotion/cmabsolutealtitudedata)
- [CMBatchedSensorManager](https://developer.apple.com/documentation/coremotion/cmbatchedsensormanager)
- [CMHeadphoneMotionManager](https://developer.apple.com/documentation/coremotion/cmheadphonemotionmanager)
- [CMWaterSubmersionManager](https://developer.apple.com/documentation/coremotion/cmwatersubmersionmanager)
- [Accessing submersion data](https://developer.apple.com/documentation/coremotion/accessing-submersion-data)
- [Getting processed device-motion data](https://developer.apple.com/documentation/coremotion/getting-processed-device-motion-data)
