CMMotionActivityManager: walking, running, driving or still
Last verified October 3, 2026 · 4 min read
Activity classification is the cheapest context signal a fitness app can get: no GPS, no camera, no wearable. Core Motion keeps the classification running on its own; your app reads it. Everything below comes from Apple's CMMotionActivityManager and CMMotionActivity documentation, read 2026-10-03.
What an update contains#
Each CMMotionActivity carries a startDate ("The time at which the change in motion occurred"), a confidence, and six Booleans:
| Property | Apple's description | Since (iOS / watchOS) |
|---|---|---|
stationary | whether the device is stationary | 7.0 / 2.0 |
walking | whether the device is on a walking person | 7.0 / 2.0 |
running | whether the device is on a running person | 7.0 / 2.0 |
automotive | whether the device is in an automobile | 7.0 / 2.0 |
cycling | whether the device is in a bicycle | 8.0 / 2.0 |
unknown | whether the type of motion is unknown | 7.0 / 2.0 |
The flags overlap, and can all be false#
This is the line that breaks naive switch statements. Apple: "The motion-related properties of this class aren't mutually exclusive. In other words, it's possible for more than one of the motion-related properties to contain the value true." The example is a car at a red light, which reports both automotive and stationary. The reverse also happens: "It's also possible for all of the properties to be set to false when the device is in motion but the movement doesn't correlate to walking, running, cycling, or automotive travel." And unknown has its own meaning: it is true "when there is no way to estimate the current type of motion", for example when "the device was turned on recently and not enough motion data had been gathered".
So model the result as a set, not an enum, and decide your own precedence. Ours, as judgement: for a fitness app, running beats walking, any of the three beats stationary, and automotive overrides everything because a phone in a car should never log a workout.
Confidence has three levels and no numbers#
CMMotionActivityConfidence is .low ("Confidence is low."), .medium ("Confidence is good.") or .high ("Confidence is high."). Apple publishes no threshold, no accuracy figure and no definition of what moves a reading between levels. Treat it as an ordering you can filter on, not a probability.
Live updates: current state, then changes#
startActivityUpdates(to:withHandler:) first reports "the current motion in effect for the device", then runs the handler "only when the motion data changes". Delivery is best effort and stops while you are suspended: "If updates arrived while your app was suspended, the last update is delivered to your app when it resumes execution. To get all of the updates that occurred while your app was suspended, use the queryActivityStarting(from:to:to:withHandler:) method." Calling it again with a new block replaces the old one, and "Updates stop altogether when the motion activity manager object itself is deallocated", so keep a strong reference.
History: seven days, delayed#
queryActivityStarting(from:to:to:withHandler:) returns an array ordered by time. Two sentences from Apple matter: "A delay of up to several minutes in reported activities is expected," and "The system stores only the last seven days worth of activity data at most." An empty range is not an empty array: "If there are no samples for the specified range of time, an error object with the code CMErrorUnknown is passed to the handler block."
import CoreMotion
let activityManager = CMMotionActivityManager() // keep a strong reference
func minutesOnFoot(since start: Date, completion: @escaping (TimeInterval) -> Void) {
guard CMMotionActivityManager.isActivityAvailable() else { return completion(0) }
activityManager.queryActivityStarting(from: start, to: Date(), to: .main) { activities, error in
guard let activities, error == nil else { return completion(0) } // CMErrorUnknown = no samples
var total: TimeInterval = 0
for (current, next) in zip(activities, activities.dropFirst()) {
let onFoot = (current.walking || current.running) && !current.automotive
if onFoot && current.confidence != .low {
total += next.startDate.timeIntervalSince(current.startDate)
}
}
completion(total)
}
}
Each activity marks a change, so a segment lasts until the next one's startDate; that is how the loop measures duration.
Permission and availability#
The same rule as the pedometer: "To use this API, you must include the NSMotionUsageDescription key in your app's Info.plist file", and "If you don't include a usage description string, your app crashes when you call this API." Check isActivityAvailable() first; Apple notes "Motion data is not available on all iOS devices." authorizationStatus() arrived in iOS 11.0 and watchOS 4.0.
Pitfalls#
- Switching on a single flag. Several can be true at once, and all can be false.
- Reading an error as "no data".
CMErrorUnknownis what an empty range returns. - Scoring a workout the moment it ends. History can lag by several minutes. Query a little later, or re-query.
- Letting the manager deallocate. Updates stop with it.
- Expecting macOS.
CMMotionActivitylists macOS 15.0, but Apple'sCMMotionActivityManagerpage lists no macOS version. On macOS the documented source is the headphone activity manager (macOS 15.0), covered on the CMHeadphoneMotionManager page.
The Android equivalent is the Activity Recognition Transition API. For steps rather than activity type, see CMPedometer.
Frequently asked questions
- Can a CMMotionActivity have more than one activity flag set?
- Yes. Apple's CMMotionActivity documentation says the motion-related properties aren't mutually exclusive, and gives the example of a car stopped at a red light, which reports both automotive and stationary as true. Apple also says all of them can be false when the device is moving in a way that doesn't match walking, running, cycling or driving. Model the result as a set of flags and choose your own precedence; a switch over one value will misread real updates.
- Why does queryActivityStarting return an error instead of an empty array?
- Because that is how Apple reports an empty range. The documentation for queryActivityStarting(from:to:to:withHandler:) says that if there are no samples for the specified range of time, an error object with the code CMErrorUnknown is passed to the handler. Treat that code as no activity recorded. Remember the other two limits on the same page: the system keeps at most the last seven days of activity, and reported activities can lag by up to several minutes.
- Does CMMotionActivityManager deliver activity changes while my app is suspended?
- No. Apple says the handler passed to startActivityUpdates(to:withHandler:) runs on a best-effort basis and updates are not delivered while your app is suspended. When the app resumes, only the last update that arrived meanwhile is delivered. To recover everything that happened in between, Apple points you to queryActivityStarting(from:to:to:withHandler:), which returns the full ordered list for a time range within the last seven days.
- What do the CMMotionActivityConfidence levels actually mean?
- Apple documents three values and very little else: low is described as confidence is low, medium as confidence is good, and high as confidence is high. There is no published threshold, probability or accuracy figure behind them. Use confidence as an ordering, for example ignoring low-confidence segments when you total active minutes, and validate the filter against your own recorded sessions rather than assuming a number.
Keep reading
Next steps
Was this page useful?
Independent comparison, last reviewed October 3, 2026. Pricing, rate limits, and feature availability change often — confirm current details in each provider’s official documentation before you commit. Product and company names are trademarks of their respective owners; AIFitnessAPI is not affiliated with, endorsed by, or sponsored by any product listed here.
← All phone sensors · by AIFitnessAPI