ComposableCoreMotion Documentation

Structure Motion​Manager

@available(iOS 4.0, *)
  @available(macCatalyst 13.0, *)
  @available(macOS, unavailable)
  @available(tvOS, unavailable)
  @available(watchOS 2.0, *)
  public struct MotionManager  

A wrapper around Core Motion's CMMotionManager that exposes its functionality through effects and actions, making it easy to use with the Composable Architecture, and easy to test.

To use in your application, you can add an action to your feature's domain that represents the type of motion data you are interested in receiving. For example, if you only want motion updates, then you can add the following action:

import ComposableCoreLocation

enum FeatureAction {
  case motionUpdate(Result<DeviceMotion, NSError>)

  // Your feature's other actions:
  ...
}

This action will be sent every time the motion manager receives new device motion data.

Next, add a MotionManager type, which is a wrapper around a CMMotionManager that this library provides, to your feature's environment of dependencies:

struct FeatureEnvironment {
  var motionManager: MotionManager

  // Your feature's other dependencies:
  ...
}

Then, create a motion manager by returning an effect from our reducer. You can either do this when your feature starts up, such as when onAppear is invoked, or you can do it when a user action occurs, such as when the user taps a button.

As an example, say we want to create a motion manager and start listening for motion updates when a "Record" button is tapped. Then we can can do both of those things by executing two effects, one after the other:

let featureReducer = Reducer<FeatureState, FeatureAction, FeatureEnvironment> {
  state, action, environment in

  // A unique identifier for our location manager, just in case we want to use
  // more than one in our application.
  struct MotionManagerId: Hashable {}

  switch action {
  case .recordingButtonTapped:
    return .concatenate(
      environment.motionManager
        .create(id: MotionManagerId())
        .fireAndForget(),

      environment.motionManager
        .startDeviceMotionUpdates(
          id: MotionManagerId(),
          using: .xArbitraryZVertical,
          to: .main
        )
        .mapError { $0 as NSError }
        .catchToEffect()
        .map(AppAction.motionUpdate)
    )

  ...
  }
}

After those effects are executed you will get a steady stream of device motion updates sent to the .motionUpdate action, which you can handle in the reducer. For example, to compute how much the device is moving up and down we can take the dot product of the device's gravity vector with the device's acceleration vector, and we could store that in the feature's state:

case let .motionUpdate(.success(deviceMotion)):
   state.zs.append(
     motion.gravity.x * motion.userAcceleration.x
       + motion.gravity.y * motion.userAcceleration.y
       + motion.gravity.z * motion.userAcceleration.z
   )

case let .motionUpdate(.failure(error)):
  // Do something with the motion update failure, like show an alert.

And then later, if you want to stop receiving motion updates, such as when a "Stop" button is tapped, we can execute an effect to stop the motion manager, and even fully destroy it if we don't need the manager anymore:

case .stopButtonTapped:
  return .concatenate(
    environment.motionManager
      .stopDeviceMotionUpdates(id: MotionManagerId())
      .fireAndForget(),

    environment.motionManager
      .destroy(id: MotionManagerId())
      .fireAndForget()
  )

That is enough to implement a basic application that interacts with Core Motion.

But the true power of building your application and interfacing with Core Motion this way is the ability to instantly test how your application behaves with Core Motion. We start by creating a TestStore whose environment contains an .unimplemented version of the MotionManager. The .unimplemented function allows you to create a fully controlled version of the motion manager that does not deal with a real CMMotionManager at all. Instead, you override whichever endpoints your feature needs to supply deterministic functionality.

For example, let's test that we property start the motion manager when we tap the record button, and that we compute the z-motion correctly, and further that we stop the motion manager when we tap the stop button. We can construct a TestStore with a mock motion manager that keeps track of when the manager is created and destroyed, and further we can even substitute in a subject that we control for device motion updates. This allows us to send any data we want to for the device motion.

func testFeature() {
  let motionSubject = PassthroughSubject<DeviceMotion, Error>()
  var motionManagerIsLive = false

  let store = TestStore(
    initialState: .init(),
    reducer: appReducer,
    environment: .init(
      motionManager: .unimplemented(
        create: { _ in .fireAndForget { motionManagerIsLive = true } },
        destroy: { _ in .fireAndForget { motionManagerIsLive = false } },
        startDeviceMotionUpdates: { _, _, _ in motionSubject.eraseToEffect() },
        stopDeviceMotionUpdates: { _ in
          .fireAndForget { motionSubject.send(completion: .finished) }
        }
      )
    )
  )
}

We can then make an assertion on our store that plays a basic user script. We can simulate the situation in which a user taps the record button, then some device motion data is received, and finally the user taps the stop button. During that script of user actions we expect the motion manager to be started, then for some z-motion values to be accumulated, and finally for the motion manager to be stopped:

let deviceMotion = DeviceMotion(
  attitude: .init(quaternion: .init(x: 1, y: 0, z: 0, w: 0)),
  gravity: CMAcceleration(x: 1, y: 2, z: 3),
  heading: 0,
  magneticField: .init(field: .init(x: 0, y: 0, z: 0), accuracy: .high),
  rotationRate: .init(x: 0, y: 0, z: 0),
  timestamp: 0,
  userAcceleration: CMAcceleration(x: 4, y: 5, z: 6)
)

store.assert(
  .send(.recordingButtonTapped) {
    XCTAssertEqual(motionManagerIsLive, true)
  },
  .do { motionSubject.send(deviceMotion) },
  .receive(.motionUpdate(.success(deviceMotion))) {
    $0.zs = [32]
  },
  .send(.stopButtonTapped) {
    XCTAssertEqual(motionManagerIsLive, false)
  }
)

This is only the tip of the iceberg. We can access any part of the CMMotionManager API in this way, and instantly unlock testability with how the motion functionality integrates with our core application logic. This can be incredibly powerful, and is typically not the kind of thing one can test easily.

Nested Types

MotionManager.Properties

Initializers

init(accelerometer​Data:​attitude​Reference​Frame:​available​Attitude​Reference​Frames:​create:​destroy:​device​Motion:​gyro​Data:​is​Accelerometer​Active:​is​Accelerometer​Available:​is​Device​Motion​Active:​is​Device​Motion​Available:​is​Gyro​Active:​is​Gyro​Available:​is​Magnetometer​Active:​is​Magnetometer​Available:​magnetometer​Data:​set:​start​Accelerometer​Updates:​start​Device​Motion​Updates:​start​Gyro​Updates:​start​Magnetometer​Updates:​stop​Accelerometer​Updates:​stop​Device​Motion​Updates:​stop​Gyro​Updates:​stop​Magnetometer​Updates:​)

public init(
      accelerometerData: @escaping (AnyHashable) -> AccelerometerData?,
      attitudeReferenceFrame: @escaping (AnyHashable) -> CMAttitudeReferenceFrame,
      availableAttitudeReferenceFrames: @escaping () -> CMAttitudeReferenceFrame,
      create: @escaping (AnyHashable) -> Effect<Never, Never>,
      destroy: @escaping (AnyHashable) -> Effect<Never, Never>,
      deviceMotion: @escaping (AnyHashable) -> DeviceMotion?,
      gyroData: @escaping (AnyHashable) -> GyroData?,
      isAccelerometerActive: @escaping (AnyHashable) -> Bool,
      isAccelerometerAvailable: @escaping (AnyHashable) -> Bool,
      isDeviceMotionActive: @escaping (AnyHashable) -> Bool,
      isDeviceMotionAvailable: @escaping (AnyHashable) -> Bool,
      isGyroActive: @escaping (AnyHashable) -> Bool,
      isGyroAvailable: @escaping (AnyHashable) -> Bool,
      isMagnetometerActive: @escaping (AnyHashable) -> Bool,
      isMagnetometerAvailable: @escaping (AnyHashable) -> Bool,
      magnetometerData: @escaping (AnyHashable) -> MagnetometerData?,
      set: @escaping (AnyHashable, Properties) -> Effect<Never, Never>,
      startAccelerometerUpdates: @escaping (AnyHashable, OperationQueue) -> Effect<
        AccelerometerData, Error
      >,
      startDeviceMotionUpdates: @escaping (AnyHashable, CMAttitudeReferenceFrame, OperationQueue) ->
        Effect<DeviceMotion, Error>,
      startGyroUpdates: @escaping (AnyHashable, OperationQueue) -> Effect<GyroData, Error>,
      startMagnetometerUpdates: @escaping (AnyHashable, OperationQueue) -> Effect<
        MagnetometerData, Error
      >,
      stopAccelerometerUpdates: @escaping (AnyHashable) -> Effect<Never, Never>,
      stopDeviceMotionUpdates: @escaping (AnyHashable) -> Effect<Never, Never>,
      stopGyroUpdates: @escaping (AnyHashable) -> Effect<Never, Never>,
      stopMagnetometerUpdates: @escaping (AnyHashable) -> Effect<Never, Never>
    )  

Properties

live

public static let live  

Methods

accelerometer​Data(id:​)

public func accelerometerData(id: AnyHashable) -> AccelerometerData?  

The latest sample of accelerometer data.

attitude​Reference​Frame(id:​)

public func attitudeReferenceFrame(id: AnyHashable) -> CMAttitudeReferenceFrame  

Returns either the reference frame currently being used or the default attitude reference frame.

create(id:​)

public func create(id: AnyHashable) -> Effect<Never, Never>  

Creates a motion manager.

A motion manager must be first created before you can use its functionality, such as starting device motion updates or accessing data directly from the manager.

destroy(id:​)

public func destroy(id: AnyHashable) -> Effect<Never, Never>  

Destroys a currently running motion manager.

In is good practice to destroy a motion manager once you are done with it, such as when you leave a screen or no longer need motion data.

device​Motion(id:​)

public func deviceMotion(id: AnyHashable) -> DeviceMotion?  

The latest sample of device-motion data.

gyro​Data(id:​)

public func gyroData(id: AnyHashable) -> GyroData?  

The latest sample of gyroscope data.

is​Accelerometer​Active(id:​)

public func isAccelerometerActive(id: AnyHashable) -> Bool  

A Boolean value that indicates whether accelerometer updates are currently happening.

is​Accelerometer​Available(id:​)

public func isAccelerometerAvailable(id: AnyHashable) -> Bool  

A Boolean value that indicates whether an accelerometer is available on the device.

is​Device​Motion​Active(id:​)

public func isDeviceMotionActive(id: AnyHashable) -> Bool  

A Boolean value that determines whether the app is receiving updates from the device-motion service.

is​Device​Motion​Available(id:​)

public func isDeviceMotionAvailable(id: AnyHashable) -> Bool  

A Boolean value that indicates whether the device-motion service is available on the device.

is​Gyro​Active(id:​)

public func isGyroActive(id: AnyHashable) -> Bool  

A Boolean value that determines whether gyroscope updates are currently happening.

is​Gyro​Available(id:​)

public func isGyroAvailable(id: AnyHashable) -> Bool  

A Boolean value that indicates whether a gyroscope is available on the device.

is​Magnetometer​Active(id:​)

public func isMagnetometerActive(id: AnyHashable) -> Bool  

A Boolean value that determines whether magnetometer updates are currently happening.

is​Magnetometer​Available(id:​)

public func isMagnetometerAvailable(id: AnyHashable) -> Bool  

A Boolean value that indicates whether a magnetometer is available on the device.

magnetometer​Data(id:​)

public func magnetometerData(id: AnyHashable) -> MagnetometerData?  

The latest sample of magnetometer data.

set(id:​accelerometer​Update​Interval:​device​Motion​Update​Interval:​gyro​Update​Interval:​magnetometer​Update​Interval:​shows​Device​Movement​Display:​)

public func set(
      id: AnyHashable,
      accelerometerUpdateInterval: TimeInterval? = nil,
      deviceMotionUpdateInterval: TimeInterval? = nil,
      gyroUpdateInterval: TimeInterval? = nil,
      magnetometerUpdateInterval: TimeInterval? = nil,
      showsDeviceMovementDisplay: Bool? = nil
    ) -> Effect<Never, Never>  

Sets certain properties on the motion manager.

start​Accelerometer​Updates(id:​to:​)

public func startAccelerometerUpdates(
      id: AnyHashable,
      to queue: OperationQueue = .main
    ) -> Effect<AccelerometerData, Error>  

Starts accelerometer updates without a handler.

Returns a long-living effect that emits accelerometer data each time the motion manager receives a new value.

start​Device​Motion​Updates(id:​using:​to:​)

public func startDeviceMotionUpdates(
      id: AnyHashable,
      using referenceFrame: CMAttitudeReferenceFrame,
      to queue: OperationQueue = .main
    ) -> Effect<DeviceMotion, Error>  

Starts device-motion updates without a block handler.

Returns a long-living effect that emits device motion data each time the motion manager receives a new value.

start​Gyro​Updates(id:​to:​)

public func startGyroUpdates(
      id: AnyHashable,
      to queue: OperationQueue = .main
    ) -> Effect<GyroData, Error>  

Starts gyroscope updates without a handler.

Returns a long-living effect that emits gyro data each time the motion manager receives a new value.

start​Magnetometer​Updates(id:​to:​)

public func startMagnetometerUpdates(
      id: AnyHashable,
      to queue: OperationQueue = .main
    ) -> Effect<MagnetometerData, Error>  

Starts magnetometer updates without a block handler.

Returns a long-living effect that emits magnetometer data each time the motion manager receives a new value.

stop​Accelerometer​Updates(id:​)

public func stopAccelerometerUpdates(id: AnyHashable) -> Effect<Never, Never>  

Stops accelerometer updates.

stop​Device​Motion​Updates(id:​)

public func stopDeviceMotionUpdates(id: AnyHashable) -> Effect<Never, Never>  

Stops device-motion updates.

stop​Gyro​Updates(id:​)

public func stopGyroUpdates(id: AnyHashable) -> Effect<Never, Never>  

Stops gyroscope updates.

stop​Magnetometer​Updates(id:​)

public func stopMagnetometerUpdates(id: AnyHashable) -> Effect<Never, Never>  

Stops magnetometer updates.

unimplemented(accelerometer​Data:​attitude​Reference​Frame:​available​Attitude​Reference​Frames:​create:​destroy:​device​Motion:​gyro​Data:​is​Accelerometer​Active:​is​Accelerometer​Available:​is​Device​Motion​Active:​is​Device​Motion​Available:​is​Gyro​Active:​is​Gyro​Available:​is​Magnetometer​Active:​is​Magnetometer​Available:​magnetometer​Data:​set:​start​Accelerometer​Updates:​start​Device​Motion​Updates:​start​Gyro​Updates:​start​Magnetometer​Updates:​stop​Accelerometer​Updates:​stop​Device​Motion​Updates:​stop​Gyro​Updates:​stop​Magnetometer​Updates:​)

public static func unimplemented(
      accelerometerData: @escaping (AnyHashable) -> AccelerometerData? = { _ in
        _unimplemented("accelerometerData")
      },
      attitudeReferenceFrame: @escaping (AnyHashable) -> CMAttitudeReferenceFrame = { _ in
        _unimplemented("attitudeReferenceFrame")
      },
      availableAttitudeReferenceFrames: @escaping () -> CMAttitudeReferenceFrame = {
        _unimplemented("availableAttitudeReferenceFrames")
      },
      create: @escaping (AnyHashable) -> Effect<Never, Never> = { _ in _unimplemented("create") },
      destroy: @escaping (AnyHashable) -> Effect<Never, Never> = { _ in _unimplemented("destroy") },
      deviceMotion: @escaping (AnyHashable) -> DeviceMotion? = { _ in _unimplemented("deviceMotion")
      },
      gyroData: @escaping (AnyHashable) -> GyroData? = { _ in _unimplemented("gyroData") },
      isAccelerometerActive: @escaping (AnyHashable) -> Bool = { _ in
        _unimplemented("isAccelerometerActive")
      },
      isAccelerometerAvailable: @escaping (AnyHashable) -> Bool = { _ in
        _unimplemented("isAccelerometerAvailable")
      },
      isDeviceMotionActive: @escaping (AnyHashable) -> Bool = { _ in
        _unimplemented("isDeviceMotionActive")
      },
      isDeviceMotionAvailable: @escaping (AnyHashable) -> Bool = { _ in
        _unimplemented("isDeviceMotionAvailable")
      },
      isGyroActive: @escaping (AnyHashable) -> Bool = { _ in _unimplemented("isGyroActive") },
      isGyroAvailable: @escaping (AnyHashable) -> Bool = { _ in _unimplemented("isGyroAvailable") },
      isMagnetometerActive: @escaping (AnyHashable) -> Bool = { _ in
        _unimplemented("isMagnetometerActive")
      },
      isMagnetometerAvailable: @escaping (AnyHashable) -> Bool = { _ in
        _unimplemented("isMagnetometerAvailable")
      },
      magnetometerData: @escaping (AnyHashable) -> MagnetometerData? = { _ in
        _unimplemented("magnetometerData")
      },
      set: @escaping (AnyHashable, MotionManager.Properties) -> Effect<Never, Never> = { _, _ in
        _unimplemented("set")
      },
      startAccelerometerUpdates: @escaping (AnyHashable, OperationQueue) -> Effect<
        AccelerometerData, Error
      > = { _, _ in _unimplemented("startAccelerometerUpdates") },
      startDeviceMotionUpdates: @escaping (AnyHashable, CMAttitudeReferenceFrame, OperationQueue) ->
        Effect<DeviceMotion, Error> = { _, _, _ in _unimplemented("startDeviceMotionUpdates") },
      startGyroUpdates: @escaping (AnyHashable, OperationQueue) -> Effect<GyroData, Error> = {
        _, _ in
        _unimplemented("startGyroUpdates")
      },
      startMagnetometerUpdates: @escaping (AnyHashable, OperationQueue) -> Effect<
        MagnetometerData, Error
      > = { _, _ in _unimplemented("startMagnetometerUpdates") },
      stopAccelerometerUpdates: @escaping (AnyHashable) -> Effect<Never, Never> = { _ in
        _unimplemented("stopAccelerometerUpdates")
      },
      stopDeviceMotionUpdates: @escaping (AnyHashable) -> Effect<Never, Never> = { _ in
        _unimplemented("stopDeviceMotionUpdates")
      },
      stopGyroUpdates: @escaping (AnyHashable) -> Effect<Never, Never> = { _ in
        _unimplemented("stopGyroUpdates")
      },
      stopMagnetometerUpdates: @escaping (AnyHashable) -> Effect<Never, Never> = { _ in
        _unimplemented("stopMagnetometerUpdates")
      }
    ) -> MotionManager