core-motion by dpearson2699/swift-ios-skills
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill core-motion在 iOS 和 watchOS 上读取设备传感器数据——加速度计、陀螺仪、磁力计、计步器和活动识别。CoreMotion 将原始传感器输入融合为处理后的设备运动数据,并为健身和导航用例提供计步器/活动 API。目标版本 Swift 6.2 / iOS 26+。
在 Info.plist 中添加 NSMotionUsageDescription,并提供一个面向用户的字符串,解释您的应用为何需要运动数据。没有此键,应用将在首次访问时崩溃。
<key>NSMotionUsageDescription</key>
<string>此应用使用运动数据来追踪您的活动。</string>
CoreMotion 对计步器和活动 API 使用 CMAuthorizationStatus。传感器 API(加速度计、陀螺仪)不需要显式授权,但需要用途描述键。
import CoreMotion
let status = CMMotionActivityManager.authorizationStatus()
switch status {
case .notDetermined:
// 首次使用时将提示
break
case .authorized:
break
case .restricted, .denied:
// 引导用户前往设置
break
@unknown default:
break
}
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
每个应用应创建恰好一个 CMMotionManager。多个实例会降低传感器更新速率。
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)")
}
// 完成后:
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()
对于游戏,可以启动更新但不提供处理程序,并在每帧轮询最新的样本:
motionManager.startAccelerometerUpdates()
// 在您的游戏循环 / 显示链接中:
if let data = motionManager.accelerometerData {
let tilt = data.acceleration.x
// 基于倾斜度移动玩家
}
设备运动将加速度计、陀螺仪和磁力计融合到一个 CMDeviceMotion 对象中,包含姿态、用户加速度(去除重力)、旋转速率和校准后的磁场。
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 // 横滚、俯仰、偏航
let userAccel = motion.userAcceleration
let gravity = motion.gravity
let heading = motion.heading // 0-360 度(需要磁力计)
print("俯仰: \(attitude.pitch), 横滚: \(attitude.roll)")
}
motionManager.stopDeviceMotionUpdates()
| 参考系 | 用例 |
|---|---|
.xArbitraryZVertical | 默认。Z 轴垂直,X 轴起始时任意方向。大多数游戏。 |
.xArbitraryCorrectedZVertical | 同上,但会随时间校正陀螺仪漂移。 |
.xMagneticNorthZVertical | X 轴指向磁北。需要磁力计。 |
.xTrueNorthZVertical | X 轴指向真北。需要磁力计 + 定位。 |
使用前检查可用参考系:
let available = CMMotionManager.availableAttitudeReferenceFrames()
if available.contains(.xTrueNorthZVertical) {
// 可以安全使用真北
}
CMPedometer 提供步数、距离、配速、步频和楼层计数。
let pedometer = CMPedometer()
guard CMPedometer.isStepCountingAvailable() else { return }
// 历史查询
pedometer.queryPedometerData(
from: Calendar.current.startOfDay(for: Date()),
to: Date()
) { data, error in
guard let data else { return }
print("今日步数: \(data.numberOfSteps)")
print("距离: \(data.distance?.doubleValue ?? 0) 米")
print("上行楼层: \(data.floorsAscended?.intValue ?? 0)")
}
// 实时更新
pedometer.startUpdates(from: Date()) { data, error in
guard let data else { return }
print("步数: \(data.numberOfSteps)")
}
// 完成后停止
pedometer.stopUpdates()
| 方法 | 检查内容 |
|---|---|
isStepCountingAvailable() | 计步器硬件 |
isDistanceAvailable() | 距离估算 |
isFloorCountingAvailable() | 用于楼层的压力高度计 |
isPaceAvailable() | 配速数据 |
isCadenceAvailable() | 步频数据 |
检测用户是静止、步行、跑步、骑行还是在车辆中。
let activityManager = CMMotionActivityManager()
guard CMMotionActivityManager.isActivityAvailable() else { return }
// 实时活动更新
activityManager.startActivityUpdates(to: .main) { activity in
guard let activity else { return }
if activity.walking {
print("步行 (置信度: \(activity.confidence.rawValue))")
} else if activity.running {
print("跑步")
} else if activity.automotive {
print("在车辆中")
} else if activity.cycling {
print("骑行")
} else if activity.stationary {
print("静止")
}
}
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("相对海拔: \(data.relativeAltitude) 米")
print("气压: \(data.pressure) kPa")
}
altimeter.stopRelativeAltitudeUpdates()
对于绝对海拔(基于 GPS):
guard CMAltimeter.isAbsoluteAltitudeAvailable() else { return }
altimeter.startAbsoluteAltitudeUpdates(to: .main) { data, error in
guard let data else { return }
print("海拔: \(data.altitude)m, 精度: \(data.accuracy)m")
}
altimeter.stopAbsoluteAltitudeUpdates()
| 间隔 | 赫兹 | 用例 | 电池影响 |
|---|---|---|---|
1.0 / 10.0 | 10 | UI 方向 | 低 |
1.0 / 30.0 | 30 | 休闲游戏 | 中等 |
1.0 / 60.0 | 60 | 动作游戏 | 高 |
1.0 / 100.0 | 100 | 最大速率 (iPhone) | 非常高 |
使用能满足您需求的最低频率。CMMotionManager 每个样本上限为 100 Hz。对于更高频率,在 watchOS/iOS 17+ 上使用 CMBatchedSensorManager。
// 错误 —— 降低所有实例的更新速率
class ViewA { let motion = CMMotionManager() }
class ViewB { let motion = CMMotionManager() }
// 正确 —— 单个实例,在整个应用中共享
@Observable
final class MotionService {
static let shared = MotionService()
let manager = CMMotionManager()
}
// 错误 —— 在没有陀螺仪的设备上会崩溃
motionManager.startGyroUpdates(to: .main) { data, _ in }
// 正确 —— 先检查
guard motionManager.isGyroAvailable else {
showUnsupportedMessage()
return
}
motionManager.startGyroUpdates(to: .main) { data, _ in }
// 错误 —— 更新持续运行,消耗电池
class MotionVC: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
motionManager.startAccelerometerUpdates(to: .main) { _, _ in }
}
// 缺少 viewDidDisappear 中的停止操作!
}
// 正确 —— 在对应的生命周期方法中停止
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
motionManager.stopAccelerometerUpdates()
}
// 错误 —— 罗盘显示使用 100 Hz
motionManager.deviceMotionUpdateInterval = 1.0 / 100.0
// 正确 —— 罗盘显示 10 Hz 绰绰有余
motionManager.deviceMotionUpdateInterval = 1.0 / 10.0
// 错误 —— 只检查一个属性
if activity.walking { handleWalking() }
// 正确 —— 多个属性可能同时为真;检查置信度
if activity.walking && activity.confidence == .high {
handleWalking()
} else if activity.automotive && activity.confidence != .low {
handleDriving()
}
NSMotionUsageDescription 并附有清晰解释CMMotionManager 实例isAccelerometerAvailable 等)start*Updates 调用在对应的生命周期方法中都有匹配的 stop*UpdatesCMMotionActivity.confidencereferences/motion-patterns.md每周安装量
334
代码仓库
GitHub 星标数
269
首次出现
2026年3月8日
安全审计
安装于
codex331
opencode328
github-copilot328
amp328
cline328
kimi-cli328
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.2 / 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 typereferences/motion-patterns.mdWeekly Installs
334
Repository
GitHub Stars
269
First Seen
Mar 8, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
codex331
opencode328
github-copilot328
amp328
cline328
kimi-cli328
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
106,200 周安装