https://github.com/pointfreeco/composable-core-motion
A library that bridges the Composable Architecture and Core Motion.
https://github.com/pointfreeco/composable-core-motion
Last synced: about 2 months ago
JSON representation
A library that bridges the Composable Architecture and Core Motion.
- Host: GitHub
- URL: https://github.com/pointfreeco/composable-core-motion
- Owner: pointfreeco
- License: mit
- Created: 2020-10-29T15:43:42.000Z (over 4 years ago)
- Default Branch: main
- Last Pushed: 2024-07-05T17:46:42.000Z (11 months ago)
- Last Synced: 2025-04-10T22:02:32.115Z (about 2 months ago)
- Language: Swift
- Homepage:
- Size: 104 KB
- Stars: 32
- Watchers: 3
- Forks: 9
- Open Issues: 3
-
Metadata Files:
- Readme: README.md
- License: LICENSE
- Code of conduct: .github/CODE_OF_CONDUCT.md
Awesome Lists containing this project
README
# Composable Core Motion
[](https://github.com/pointfreeco/composable-core-motion/actions?query=workflow%3ACI)
[](https://swiftpackageindex.com/pointfreeco/composable-core-motion)
[](https://swiftpackageindex.com/pointfreeco/composable-core-motion)Composable Core Motion is library that bridges [the Composable Architecture](https://github.com/pointfreeco/swift-composable-architecture) and [Core Motion](https://developer.apple.com/documentation/coremotion).
* [Example](#example)
* [Basic usage](#basic-usage)
* [Installation](#installation)
* [Documentation](#documentation)
* [Help](#help)## Example
Check out the [MotionManager](./Examples/MotionManager) demo to see ComposableCoreMotion in practice.
## Basic Usage
To use ComposableCoreMotion 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:
```swift
import ComposableCoreMotionenum FeatureAction {
case motionUpdate(Result)// 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:
```swift
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:
```swift
let featureReducer = Reducer {
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:
```swift
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:
```swift
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.
```swift
func testFeature() {
let motionSubject = PassthroughSubject()
var motionManagerIsLive = falselet 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:
```swift
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.
## Installation
You can add ComposableCoreMotion to an Xcode project by adding it as a package dependency.
1. From the **File** menu, select **Swift Packages › Add Package Dependency…**
2. Enter "https://github.com/pointfreeco/composable-core-motion" into the package repository URL text field## Documentation
The latest documentation for the Composable Core Motion APIs is available [here](https://pointfreeco.github.io/composable-core-motion/).
## Help
If you want to discuss Composable Core Motion and the Composable Architecture, or have a question about how to use them to solve a particular problem, ask around on [its Swift forum](https://forums.swift.org/c/related-projects/swift-composable-architecture).
## License
This library is released under the MIT license. See [LICENSE](LICENSE) for details.