可组合的核心运动 (Composable Core Motion)

CI

可组合的核心运动是一个库,它桥接了 可组合架构 (Composable Architecture)Core Motion

示例

查看 MotionManager 示例,以了解 ComposableCoreMotion 的实际应用。

基本用法

要在你的应用程序中使用 ComposableCoreMotion,你可以向你的功能的域添加一个 action,该 action 代表你感兴趣接收的运动数据类型。例如,如果你只想要运动更新,那么你可以添加以下 action:

import ComposableCoreMotion

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

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

每次运动管理器接收到新的设备运动数据时,都会发送此 action。

接下来,添加一个 MotionManager 类型,它是对该库提供的 CMMotionManager 的包装器,到你的功能的依赖项环境中:

struct FeatureEnvironment {
  var motionManager: MotionManager

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

然后,通过从我们的 reducer 返回一个 effect 来创建一个运动管理器。你可以选择在你的功能启动时执行此操作,例如当调用 onAppear 时,或者你可以在用户操作发生时执行此操作,例如当用户点击按钮时。

例如,假设我们想要创建一个运动管理器并开始监听运动更新,当点击“记录”按钮时。那么我们可以通过执行两个 effect 来实现这两个目标,一个接一个:

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)
    )

  ...
  }
}

在执行这些 effect 后,你将获得源源不断的设备运动更新,这些更新将发送到 .motionUpdate action,你可以在 reducer 中处理它。例如,为了计算设备上下移动了多少,我们可以将设备的重力矢量与设备的加速度矢量进行点积运算,并将其存储在功能的状态中:

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.

然后,稍后,如果你想停止接收运动更新,例如当点击“停止”按钮时,我们可以执行一个 effect 来停止运动管理器,甚至完全销毁它,如果不再需要该管理器:

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

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

这足以实现一个与 Core Motion 交互的基本应用程序。

但是,以这种方式构建你的应用程序并与 Core Motion 交互的真正力量在于能够立即测试你的应用程序在 Core Motion 中的行为。我们首先创建一个 TestStore,其环境包含 MotionManager.unimplemented 版本。 .unimplemented 函数允许你创建一个完全受控的运动管理器版本,该版本根本不处理真实的 CMMotionManager。相反,你可以覆盖你的功能需要提供的任何端点,以提供确定性的功能。

例如,让我们测试一下当我们点击记录按钮时是否正确启动了运动管理器,以及我们是否正确计算了 z 轴运动,以及当我们点击停止按钮时是否停止了运动管理器。 我们可以构建一个 TestStore,其中包含一个模拟运动管理器,该管理器跟踪何时创建和销毁该管理器,并且我们甚至可以为设备运动更新替换一个我们控制的 subject。 这允许我们为设备运动发送我们想要的任何数据。

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) }
        }
      )
    )
  )
}

然后,我们可以对我们的 store 进行断言,以播放一个基本的用户脚本。 我们可以模拟用户点击记录按钮,然后接收到一些设备运动数据,最后用户点击停止按钮的情况。 在用户操作的脚本中,我们期望运动管理器启动,然后累积一些 z 轴运动值,最后停止运动管理器。

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)
  }
)

这只是冰山一角。 我们可以以这种方式访问 CMMotionManager API 的任何部分,并立即解锁可测试性,了解运动功能如何与我们的核心应用程序逻辑集成。 这可能非常强大,而且通常不是人们可以轻松测试的东西。

安装

你可以通过将 ComposableCoreMotion 作为包依赖项添加到 Xcode 项目。

  1. 文件菜单中,选择Swift Packages › Add Package Dependency…
  2. 在包存储库 URL 文本字段中输入“https://github.com/pointfreeco/composable-core-motion

文档

有关可组合的核心运动 API 的最新文档,请访问 此处

帮助

如果你想讨论可组合的核心运动和可组合架构,或者对如何使用它们解决特定问题有疑问,请在 其 Swift 论坛 上提问。

许可

该库是在 MIT 许可下发布的。 有关详细信息,请参见 LICENSE