关于 core-motion
面向 Swift 6.3 和 iOS 26+ 开发者的 CoreMotion 技能,说明如何读取运动、计步、活动识别与高度数据,并关注传感器可用性和耗电。
主要功能
- 01读取加速度计与陀螺仪数据
- 02获取融合设备运动数据
- 03查询步数、距离与楼层
- 04识别行走、跑步等活动
- 05读取相对及绝对高度
- 06设置采样间隔并管理耗电
适用场景
- 01构建基于倾斜的游戏控制
- 02记录步数与移动距离
- 03识别用户活动状态
- 04监测设备高度与气压变化
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dpearson2699/ swift-ios-skills --skill core-motion安装后在 Claude Code 中输入/core-motion确认已加载。
附带文件evals/evals.jsonreferences/motion-patterns.md
详细介绍
CoreMotion
使用 Core Motion 读取设备运动、计步器/活动、海拔、耳机、批量健身和浸水传感器。适用范围:Swift 6.3、iOS 26 及更高版本。
目录
- 设置
- CMMotionManager:传感器数据
- 处理后的设备运动数据
- CMPedometer:步数和距离数据
- CMMotionActivityManager:活动识别
- CMAltimeter:海拔数据
- 更新间隔和电池
- 常见错误
- 审核清单
- 参考资料
设置
Info.plist
在 Info.plist 中添加 NSMotionUsageDescription,并提供面向用户的说明,解释应用为何需要运动数据。没有此键,应用首次访问时会崩溃。
<key>NSMotionUsageDescription</key>
<string>This app uses motion data to track your activity.</string>
授权
当 API 提供授权状态时,使用对应管理器的 authorizationStatus() 或 authorizationStatus 属性(CMPedometer、CMMotionActivityManager、CMAltimeter、耳机运动、批量传感器和浸水功能)。原始 CMMotionManager 加速度计/陀螺仪/设备运动数据流没有显式的授权请求 API;仍需提供用途说明,并处理启动/更新回调中的错误。
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:传感器数据
每个应用只创建一个 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)")
}
// 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()
轮询模式(游戏)
对于游戏,无需处理器即可启动更新,并在每帧轮询最新样本:
motionManager.startAccelerometerUpdates()
// In your game loop / display link:
if let data = motionManager.accelerometerData {
let tilt = data.acceleration.x
// Move player based on tilt
}
处理后的设备运动数据
设备运动会将加速度计、陀螺仪和磁力计的数据融合到单个 CMDeviceMotion 对象中,其中包含姿态、用户加速度(已移除重力)、旋转速率和经过校准的磁场。
提供设备运动方面的指导时,应在代码片段中展示运行时帧检查,而不是硬编码已校正、磁北或真北坐标系。如果首选坐标系不可用,则回退到 .xArbitraryZVertical。
guard motionManager.isDeviceMotionAvailable else { return }
let availableFrames = CMMotionManager.availableAttitudeReferenceFrames()
let frame: CMAttitudeReferenceFrame = availableFrames.contains(.xArbitraryCorrectedZVertical)
? .xArbitraryCorrectedZVertical
: .xArbitraryZVertical
motionManager.deviceMotionUpdateInterval = 1.0 / 60.0
motionManager.startDeviceMotionUpdates(
using: frame,
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 // degrees relative to the current frame
print("Pitch: \(attitude.pitch), Roll: \(attitude.roll)")
}
motionManager.stopDeviceMotionUpdates()
姿态参考坐标系
对于简单的倾斜控制,请使用 .xArbitraryZVertical 或 .xArbitraryCorrectedZVertical;它们无需依赖磁力计或位置数据。请求已校正、磁北或真北坐标系之前,先调用 CMMotionManager.availableAttitudeReferenceFrames(),并回退到可用坐标系。
| 坐标系 | 使用场景 |
|---|---|
.xArbitraryZVertical | 默认值。Z 轴为垂直方向,X 轴在启动时任意确定。适用于大多数游戏。 |
.xArbitraryCorrectedZVertical | 同上,并会随时间校正陀螺仪漂移。 |
.xMagneticNorthZVertical | X 轴指向磁北。需要磁力计。 |
.xTrueNorthZVertical | X 轴指向真北。需要磁力计和位置数据。 |
使用前检查可用坐标系:
let available = CMMotionManager.availableAttitudeReferenceFrames()
if available.contains(.xTrueNorthZVertical) {
// Safe to use true north
}
CMPedometer:步数和距离数据
CMPedometer 提供步数、距离、配速、步频和楼层计数。
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()
可用性检查
| 方法 | 检查内容 |
|---|---|
isStepCountingAvailable() | 计步器硬件 |
isDistanceAvailable() | 距离估算 |
isFloorCountingAvailable() | 用于楼层计数的气压高度计 |
isPaceAvailable() | 配速数据 |
isCadenceAvailable() | 步频数据 |
CMMotionActivityManager:活动识别
检测用户处于静止、步行、跑步、骑行或乘车状态。
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)")
}
}
CMAltimeter:海拔数据
气压高度计的访问权限由 NSMotionUsageDescription 控制;若运动数据访问被拒绝,请通过数据不可用和更新处理器错误来处理。
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()
绝对海拔是相对于海平面的海拔,并非基于 GPS 的海拔。请先检查可用性。绝对海拔仅适用于受支持的硬件,例如 iPhone 12 或更新机型,以及 Apple Watch Series 6、Apple Watch SE 或更新机型。
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()
更新间隔与电池续航
| 间隔 | Hz | 使用场景 | 电池影响 |
|---|---|---|---|
1.0 / 10.0 | 10 | UI 方向 | 低 |
1.0 / 30.0 | 30 | 休闲游戏 | 中等 |
1.0 / 60.0 | 60 | 动作游戏 | 高 |
1.0 / 100.0 | 100 | 最高频率(iPhone) | 非常高 |
请使用满足需求的最低频率。不要假设所有设备的最高采样率都相同。对于高频运动健身动作追踪,在支持的情况下,请使用
CMBatchedSensorManager,并读取其报告的
accelerometerDataFrequency 或 deviceMotionDataFrequency,而不是为这些只读属性赋值。
常见错误
不要:创建多个 CMMotionManager 实例
在应用级别保留一个 CMMotionManager;多个实例相互竞争可能会降低更新速率。
不要:跳过传感器可用性检查
在启动每个传感器数据流之前,立即应用对应的 is...Available 检查。
不要:忘记停止更新
每次启动都应在对应的生命周期处理或任务取消路径中调用相应的停止方法。
不要:使用不必要的高更新速率
选择满足交互需求的最低速率,并将更新间隔与电池续航表格作为起点。
不要:假设所有 CMMotionActivity 属性都互斥
// 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()
}
审查清单
- 在 Info.plist 中提供
NSMotionUsageDescription,并附上清晰说明 - 整个应用共享单个
CMMotionManager实例 - 启动更新前检查传感器可用性(
isAccelerometerAvailable等) - 调用计步器/活动 API 前检查授权状态
- 将更新间隔设为可接受的最低频率
- 所有
start*Updates调用都在对应的生命周期处理处配有相应的stop*Updates调用 - 将处理程序分派到适当的队列(重度处理不得阻塞主线程)
- 根据活动类型采取行动前,检查
CMMotionActivity.confidence - 检查更新处理程序中的错误参数
- 设备运动代码片段在请求特定姿态参考系前,先调用
CMMotionManager.availableAttitudeReferenceFrames() - 根据实际需求选择姿态参考系(不要不必要地默认使用真北)