core-motion

Access accelerometer, gyroscope, magnetometer, pedometer, and activity-recognition data using CoreMotion. Use when reading device sensor data, counting steps,…

INSTALLATION
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill core-motion
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

$27

Setup

Info.plist

Add NSMotionUsageDescription to Info.plist with a user-facing string explaining

why your app needs motion data. Without this key, the app crashes on first access.

<key>NSMotionUsageDescription</key>

<string>This app uses motion data to track your activity.</string>

Authorization

CoreMotion uses CMAuthorizationStatus for pedometer and activity APIs. Sensor

APIs (accelerometer, gyro) do not require explicit authorization but do require

the usage description key.

import CoreMotion

let status = CMMotionActivityManager.authorizationStatus()

switch status {

case .notDetermined:

    // Will prompt on first use

    break

case .authorized:

    break

case .restricted, .denied:

    // Direct user to Settings

    break

@unknown default:

    break

}

CMMotionManager: Sensor Data

Create exactly one CMMotionManager per app. Multiple instances degrade

sensor update rates.

import CoreMotion

let motionManager = CMMotionManager()

Accelerometer Updates

guard motionManager.isAccelerometerAvailable else { return }

motionManager.accelerometerUpdateInterval = 1.0 / 60.0  // 60 Hz

motionManager.startAccelerometerUpdates(to: .main) { data, error in

    guard let acceleration = data?.acceleration else { return }

    print("x: \(acceleration.x), y: \(acceleration.y), z: \(acceleration.z)")

}

// When done:

motionManager.stopAccelerometerUpdates()

Gyroscope Updates

guard motionManager.isGyroAvailable else { return }

motionManager.gyroUpdateInterval = 1.0 / 60.0

motionManager.startGyroUpdates(to: .main) { data, error in

    guard let rotationRate = data?.rotationRate else { return }

    print("x: \(rotationRate.x), y: \(rotationRate.y), z: \(rotationRate.z)")

}

motionManager.stopGyroUpdates()

Polling Pattern (Games)

For games, start updates without a handler and poll the latest sample each frame:

motionManager.startAccelerometerUpdates()

// In your game loop / display link:

if let data = motionManager.accelerometerData {

    let tilt = data.acceleration.x

    // Move player based on tilt

}

Processed Device Motion

Device motion fuses accelerometer, gyroscope, and magnetometer into a single

CMDeviceMotion object with attitude, user acceleration (gravity removed),

rotation rate, and calibrated magnetic field.

guard motionManager.isDeviceMotionAvailable else { return }

motionManager.deviceMotionUpdateInterval = 1.0 / 60.0

motionManager.startDeviceMotionUpdates(

    using: .xArbitraryZVertical,

    to: .main

) { motion, error in

    guard let motion else { return }

    let attitude = motion.attitude       // roll, pitch, yaw

    let userAccel = motion.userAcceleration

    let gravity = motion.gravity

    let heading = motion.heading         // 0-360 degrees (requires magnetometer)

    print("Pitch: \(attitude.pitch), Roll: \(attitude.roll)")

}

motionManager.stopDeviceMotionUpdates()

Attitude Reference Frames

Frame

Use Case

.xArbitraryZVertical

Default. Z is vertical, X arbitrary at start. Most games.

.xArbitraryCorrectedZVertical

Same as above, corrected for gyro drift over time.

.xMagneticNorthZVertical

X points to magnetic north. Requires magnetometer.

.xTrueNorthZVertical

X points to true north. Requires magnetometer + location.

Check available frames before use:

let available = CMMotionManager.availableAttitudeReferenceFrames()

if available.contains(.xTrueNorthZVertical) {

    // Safe to use true north

}

CMPedometer: Step and Distance Data

CMPedometer provides step counts, distance, pace, cadence, and floor counts.

let pedometer = CMPedometer()

guard CMPedometer.isStepCountingAvailable() else { return }

// Historical query

pedometer.queryPedometerData(

    from: Calendar.current.startOfDay(for: Date()),

    to: Date()

) { data, error in

    guard let data else { return }

    print("Steps today: \(data.numberOfSteps)")

    print("Distance: \(data.distance?.doubleValue ?? 0) meters")

    print("Floors up: \(data.floorsAscended?.intValue ?? 0)")

}

// Live updates

pedometer.startUpdates(from: Date()) { data, error in

    guard let data else { return }

    print("Steps: \(data.numberOfSteps)")

}

// Stop when done

pedometer.stopUpdates()

Availability Checks

Method

What It Checks

isStepCountingAvailable()

Step counter hardware

isDistanceAvailable()

Distance estimation

isFloorCountingAvailable()

Barometric altimeter for floors

isPaceAvailable()

Pace data

isCadenceAvailable()

Cadence data

CMMotionActivityManager: Activity Recognition

Detects whether the user is stationary, walking, running, cycling, or in a vehicle.

let activityManager = CMMotionActivityManager()

guard CMMotionActivityManager.isActivityAvailable() else { return }

// Live activity updates

activityManager.startActivityUpdates(to: .main) { activity in

    guard let activity else { return }

    if activity.walking {

        print("Walking (confidence: \(activity.confidence.rawValue))")

    } else if activity.running {

        print("Running")

    } else if activity.automotive {

        print("In vehicle")

    } else if activity.cycling {

        print("Cycling")

    } else if activity.stationary {

        print("Stationary")

    }

}

activityManager.stopActivityUpdates()

Historical Activity Query

let yesterday = Calendar.current.date(byAdding: .day, value: -1, to: Date())!

activityManager.queryActivityStarting(

    from: yesterday,

    to: Date(),

    to: .main

) { activities, error in

    guard let activities else { return }

    for activity in activities {

        print("\(activity.startDate): walking=\(activity.walking)")

    }

}

CMAltimeter: Altitude Data

let altimeter = CMAltimeter()

guard CMAltimeter.isRelativeAltitudeAvailable() else { return }

altimeter.startRelativeAltitudeUpdates(to: .main) { data, error in

    guard let data else { return }

    print("Relative altitude: \(data.relativeAltitude) meters")

    print("Pressure: \(data.pressure) kPa")

}

altimeter.stopRelativeAltitudeUpdates()

For absolute altitude (GPS-based):

guard CMAltimeter.isAbsoluteAltitudeAvailable() else { return }

altimeter.startAbsoluteAltitudeUpdates(to: .main) { data, error in

    guard let data else { return }

    print("Altitude: \(data.altitude)m, accuracy: \(data.accuracy)m")

}

altimeter.stopAbsoluteAltitudeUpdates()

Update Intervals and Battery

Interval

Hz

Use Case

Battery Impact

1.0 / 10.0

10

UI orientation

Low

1.0 / 30.0

30

Casual games

Moderate

1.0 / 60.0

60

Action games

High

1.0 / 100.0

100

Max rate (iPhone)

Very High

Use the lowest frequency that meets your needs. CMMotionManager caps at 100 Hz

per sample. For higher frequencies, use CMBatchedSensorManager on watchOS/iOS 17+.

Common Mistakes

DON'T: Create multiple CMMotionManager instances

// WRONG -- degrades update rates for all instances

class ViewA { let motion = CMMotionManager() }

class ViewB { let motion = CMMotionManager() }

// CORRECT -- single instance, shared across the app

@Observable

final class MotionService {

    static let shared = MotionService()

    let manager = CMMotionManager()

}

DON'T: Skip sensor availability checks

// WRONG -- crashes on devices without gyroscope

motionManager.startGyroUpdates(to: .main) { data, _ in }

// CORRECT -- check first

guard motionManager.isGyroAvailable else {

    showUnsupportedMessage()

    return

}

motionManager.startGyroUpdates(to: .main) { data, _ in }

DON'T: Forget to stop updates

// WRONG -- updates keep running, draining battery

class MotionVC: UIViewController {

    override func viewDidAppear(_ animated: Bool) {

        super.viewDidAppear(animated)

        motionManager.startAccelerometerUpdates(to: .main) { _, _ in }

    }

    // Missing viewDidDisappear stop!

}

// CORRECT -- stop in the counterpart lifecycle method

override func viewDidDisappear(_ animated: Bool) {

    super.viewDidDisappear(animated)

    motionManager.stopAccelerometerUpdates()

}

DON'T: Use unnecessarily high update rates

// WRONG -- 100 Hz for a compass display

motionManager.deviceMotionUpdateInterval = 1.0 / 100.0

// CORRECT -- 10 Hz is more than enough for a compass

motionManager.deviceMotionUpdateInterval = 1.0 / 10.0

DON'T: Assume all CMMotionActivity properties are mutually exclusive

// WRONG -- checking only one property

if activity.walking { handleWalking() }

// CORRECT -- multiple can be true simultaneously; check confidence

if activity.walking &#x26;&#x26; activity.confidence == .high {

    handleWalking()

} else if activity.automotive &#x26;&#x26; activity.confidence != .low {

    handleDriving()

}

Review Checklist

  • NSMotionUsageDescription present in Info.plist with a clear explanation
  • Single CMMotionManager instance shared across the app
  • Sensor availability checked before starting updates (isAccelerometerAvailable, etc.)
  • Authorization status checked before pedometer/activity APIs
  • Update interval set to the lowest acceptable frequency
  • All start*Updates calls have matching stop*Updates in lifecycle counterparts
  • Handlers dispatched to appropriate queues (not blocking main for heavy processing)
  • CMMotionActivity.confidence checked before acting on activity type
  • Error parameters checked in update handlers
  • Attitude reference frame chosen based on actual need (not defaulting to true north unnecessarily)

References

BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card