可组合的核心运动是一个库,它桥接了 可组合架构 (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 项目。
有关可组合的核心运动 API 的最新文档,请访问 此处。
如果你想讨论可组合的核心运动和可组合架构,或者对如何使用它们解决特定问题有疑问,请在 其 Swift 论坛 上提问。
该库是在 MIT 许可下发布的。 有关详细信息,请参见 LICENSE。