From apple-kit-skills
Accesses accelerometer, gyroscope, magnetometer, pedometer, activity recognition, and altitude data using CoreMotion in Swift iOS/watchOS apps.
npx claudepluginhub dpearson2699/swift-ios-skills --plugin apple-kit-skillsThis skill uses the workspace's default tool permissions.
Read device sensor data -- accelerometer, gyroscope, magnetometer, pedometer, and
Generates design tokens/docs from CSS/Tailwind/styled-components codebases, audits visual consistency across 10 dimensions, detects AI slop in UI.
Records polished WebM UI demo videos of web apps using Playwright with cursor overlay, natural pacing, and three-phase scripting. Activates for demo, walkthrough, screen recording, or tutorial requests.
Delivers idiomatic Kotlin patterns for null safety, immutability, sealed classes, coroutines, Flows, extensions, DSL builders, and Gradle DSL. Use when writing, reviewing, refactoring, or designing Kotlin code.
Read device sensor data -- accelerometer, gyroscope, magnetometer, pedometer, and activity recognition -- on iOS and watchOS. CoreMotion fuses raw sensor inputs into processed device-motion data and provides pedometer/activity APIs for fitness and navigation use cases. Targets Swift 6.3 / iOS 26+.
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>
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
}
Create exactly one CMMotionManager per app. Multiple instances degrade
sensor update rates.
import CoreMotion
let motionManager = CMMotionManager()
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()
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()
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
}
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()
| 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 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()
| Method | What It Checks |
|---|---|
isStepCountingAvailable() | Step counter hardware |
isDistanceAvailable() | Distance estimation |
isFloorCountingAvailable() | Barometric altimeter for floors |
isPaceAvailable() | Pace data |
isCadenceAvailable() | Cadence data |
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()
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)")
}
}
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()
| 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+.
// 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()
}
// 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 }
// 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()
}
// 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
// WRONG -- checking only one property
if activity.walking { handleWalking() }
// CORRECT -- multiple can be true simultaneously; check confidence
if activity.walking && activity.confidence == .high {
handleWalking()
} else if activity.automotive && activity.confidence != .low {
handleDriving()
}
NSMotionUsageDescription present in Info.plist with a clear explanationCMMotionManager instance shared across the appisAccelerometerAvailable, etc.)start*Updates calls have matching stop*Updates in lifecycle counterpartsCMMotionActivity.confidence checked before acting on activity type