├── .github ├── brew │ ├── Brewfile │ └── Brewfile.lock.json ├── scripts │ └── lint.sh └── workflows │ └── ci.yml ├── .gitignore ├── .swiftformat ├── .swiftlint.yml ├── Package.resolved ├── Package.swift ├── README.md ├── Sources ├── ComposableAltimeter │ ├── Interface.swift │ ├── Live.swift │ ├── Mock.swift │ └── Models │ │ └── RelativeAltitudeData.swift ├── ComposableAudioPlayer │ ├── Interface.swift │ ├── Live.swift │ └── Mock.swift ├── ComposableAudioRecorder │ ├── Interface.swift │ ├── Live.swift │ └── Mock.swift ├── ComposableBluetoothCentralManager │ ├── Extensions │ │ ├── BluetoothAdvertisementData.swift │ │ ├── PeripheralScanningOptions.swift │ │ └── StateRestorationOptions.swift │ ├── Interface.swift │ ├── Live.swift │ └── Mock.swift ├── ComposableBluetoothPeripheralManager │ ├── Interface.swift │ ├── Live.swift │ └── Mock.swift ├── ComposableCoreLocation │ ├── Interface.swift │ ├── Internal │ │ └── Exports.swift │ ├── Live.swift │ ├── Mock.swift │ └── Models │ │ ├── AccuracyAuthorization.swift │ │ ├── Beacon.swift │ │ ├── Heading.swift │ │ ├── Location.swift │ │ ├── Region.swift │ │ └── Visit.swift ├── ComposableCoreMotion │ ├── HeadphoneMotionManager │ │ ├── HeadphoneMotionManagerInterface.swift │ │ ├── HeadphoneMotionManagerLive.swift │ │ └── HeadphoneMotionManagerMock.swift │ ├── Interface.swift │ ├── Internal │ │ ├── Debug.swift │ │ ├── Exports.swift │ │ └── Unimplemented.swift │ ├── Live.swift │ ├── Mock.swift │ └── Models │ │ ├── AccelerometerData.swift │ │ ├── Attitude.swift │ │ ├── DeviceMotion.swift │ │ ├── GyroData.swift │ │ └── MagnetometerData.swift ├── ComposableFast │ ├── Interface.swift │ ├── Live.swift │ ├── Mock.swift │ └── README.md ├── ComposableHealthStore │ ├── Interface.swift │ ├── Live.swift │ └── Mock.swift ├── ComposableLocalSearch │ ├── Interface.swift │ ├── Live.swift │ └── Mock.swift ├── ComposableMediaPlayer │ ├── Interface.swift │ ├── Live.swift │ └── Mock.swift ├── ComposablePlayer │ ├── Interface.swift │ ├── Live.swift │ └── Mock.swift ├── ComposableSpeechRecognizer │ ├── Interface.swift │ ├── Live.swift │ └── Mock.swift ├── ComposableSpeechSynthesizer │ ├── Interface.swift │ ├── Live.swift │ └── Mock.swift ├── ComposableWatchConnectivity │ ├── Extensions │ │ └── WCSessionActivationState+Extensions.swift │ ├── Interface.swift │ ├── Live.swift │ └── Mock.swift └── ComposableWorkout │ ├── Interface.swift │ ├── Live.swift │ └── Mock.swift └── Tests ├── ComposableAudioPlayerTests └── ComposableAudioPlayerTests.swift ├── ComposableAudioRecorderTests └── ComposableAudioRecorderTests.swift ├── ComposableBluetoothCentralManagerTests └── ComposableBluetoothCentralManagerTests.swift ├── ComposableBluetoothPeripheralManagerTests └── ComposableBluetoothPeripheralManagerTests.swift ├── ComposableCoreLocationTests └── ComposableCoreLocationTests.swift ├── ComposableCoreMotionTests └── ComposableCoreMotionTests.swift ├── ComposableFastTests └── ComposableFastTests.swift ├── ComposablePlayerTests └── ComposableAudioPlayerTests.swift ├── ComposableSpeechRecognizerTests └── ComposableSpeechRecognizerTests.swift └── ComposableSpeechSynthesizerTests └── ComposableSpeechSynthesizerTests.swift /.github/brew/Brewfile: -------------------------------------------------------------------------------- 1 | # Homebrew dependences 2 | 3 | brew "swiftformat" 4 | brew "swiftlint" 5 | -------------------------------------------------------------------------------- /.github/brew/Brewfile.lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "entries": { 3 | "brew": { 4 | "swiftformat": { 5 | "version": "0.47.10", 6 | "bottle": { 7 | "cellar": ":any_skip_relocation", 8 | "prefix": "/opt/homebrew", 9 | "files": { 10 | "big_sur": { 11 | "url": "https://homebrew.bintray.com/bottles/swiftformat-0.47.10.big_sur.bottle.tar.gz", 12 | "sha256": "304adfd624e647414d611c6bd67351b17842e496dd1efe9d70e315904a618b3d" 13 | }, 14 | "arm64_big_sur": { 15 | "url": "https://homebrew.bintray.com/bottles/swiftformat-0.47.10.arm64_big_sur.bottle.tar.gz", 16 | "sha256": "6c37b5f786958f3b2dc313749cc4e05f199a8aec7a8023b9449ce35df1cd165f" 17 | }, 18 | "catalina": { 19 | "url": "https://homebrew.bintray.com/bottles/swiftformat-0.47.10.catalina.bottle.tar.gz", 20 | "sha256": "4e430261db4146454a33dd5656fa3294bafe2bcd0626e8440b9e0c920a96b9e0" 21 | }, 22 | "mojave": { 23 | "url": "https://homebrew.bintray.com/bottles/swiftformat-0.47.10.mojave.bottle.tar.gz", 24 | "sha256": "d09620d456ab1c508bc2b6e52d2425ff643aab6ac1fb99599a63a556fce03225" 25 | } 26 | } 27 | } 28 | }, 29 | "swiftlint": { 30 | "version": "0.42.0", 31 | "bottle": { 32 | "cellar": ":any_skip_relocation", 33 | "prefix": "/opt/homebrew", 34 | "files": { 35 | "big_sur": { 36 | "url": "https://homebrew.bintray.com/bottles/swiftlint-0.42.0.big_sur.bottle.tar.gz", 37 | "sha256": "1a0540f0ff6cac2da0a51672db7963aef71cc58954c34791e9b01dafd63c5898" 38 | }, 39 | "arm64_big_sur": { 40 | "url": "https://homebrew.bintray.com/bottles/swiftlint-0.42.0.arm64_big_sur.bottle.tar.gz", 41 | "sha256": "a9cf09a414fd3b8a5cb3d94e682a725f654a4d1caef19497a500d6dbb926ba7c" 42 | }, 43 | "catalina": { 44 | "url": "https://homebrew.bintray.com/bottles/swiftlint-0.42.0.catalina.bottle.tar.gz", 45 | "sha256": "e9023ed754eb8cb78a9f2b469a90875ca42a7afffd3e96f8142252e81d889793" 46 | } 47 | } 48 | } 49 | } 50 | } 51 | }, 52 | "system": { 53 | "macos": { 54 | "big_sur": { 55 | "HOMEBREW_VERSION": "2.7.5", 56 | "HOMEBREW_PREFIX": "/usr/local", 57 | "Homebrew/homebrew-core": "6eb65915230d4fa111b0887a24c07013c966c110", 58 | "CLT": "12.3.0.0.1.1607026830", 59 | "Xcode": "12.2", 60 | "macOS": "11.1" 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /.github/scripts/lint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Pre project generation script 3 | 4 | which -s brew 5 | if [[ $? != 0 ]] ; then 6 | # Install Homebrew 7 | /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)" 8 | else 9 | brew update 10 | fi 11 | 12 | # Homebrew dependencies 13 | brew bundle --file .github/brew/Brewfile 14 | 15 | 16 | # Run linters and formatters 17 | if [[ -f ".swiftformat" ]]; then 18 | swiftformat . 19 | fi 20 | 21 | if [[ -f ".swiftlint" ]]; then 22 | swiftlint autocorrect 23 | fi -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - '*' 10 | 11 | jobs: 12 | tests: 13 | runs-on: macOS-latest 14 | strategy: 15 | matrix: 16 | xcode: 17 | - 11.3 18 | - 11.7 19 | - 12.4 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Select Xcode ${{ matrix.xcode }} 23 | run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app 24 | - name: Build 25 | run: swift build 26 | - name: Test 27 | run: swift test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | .swiftpm/ 7 | -------------------------------------------------------------------------------- /.swiftformat: -------------------------------------------------------------------------------- 1 | # file options 2 | 3 | --exclude Snapshots,Carthage,Sources/ComposableCoreLocation 4 | 5 | # format options 6 | 7 | --allman false 8 | --binarygrouping 4,8 9 | --commas always 10 | --comments indent 11 | --decimalgrouping 3,6 12 | --elseposition same-line 13 | --empty void 14 | --exponentcase lowercase 15 | --exponentgrouping disabled 16 | --fractiongrouping disabled 17 | --header "{file}\nCopyright (c) {year} Joe Blau" 18 | --hexgrouping 4,8 19 | --hexliteralcase uppercase 20 | --ifdef indent 21 | --indent 4 22 | --indentcase false 23 | --importgrouping testable-bottom 24 | --linebreaks lf 25 | --maxwidth none 26 | --octalgrouping 4,8 27 | --operatorfunc spaced 28 | --patternlet hoist 29 | --ranges spaced 30 | --self remove 31 | --semicolons inline 32 | --stripunusedargs always 33 | --swiftversion 5.1 34 | --trimwhitespace always 35 | --wraparguments preserve 36 | --wrapcollections preserve 37 | 38 | # rules 39 | 40 | --enable isEmpty 41 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | whitelist_rules: 2 | - closure_spacing 3 | - colon 4 | - empty_enum_arguments 5 | - fatal_error_message 6 | - force_cast 7 | - force_try 8 | - force_unwrapping 9 | - implicitly_unwrapped_optional 10 | - legacy_cggeometry_functions 11 | - legacy_constant 12 | - legacy_constructor 13 | - legacy_nsgeometry_functions 14 | - operator_usage_whitespace 15 | - redundant_string_enum_value 16 | - redundant_void_return 17 | - return_arrow_whitespace 18 | - trailing_newline 19 | - type_name 20 | - unused_closure_parameter 21 | - unused_optional_binding 22 | - vertical_whitespace 23 | - void_return 24 | - custom_rules 25 | 26 | colon: 27 | apply_to_dictionaries: false 28 | 29 | indentation: 2 30 | 31 | custom_rules: 32 | no_objcMembers: 33 | name: "@objcMembers" 34 | regex: "@objcMembers" 35 | message: "Explicitly use @objc on each member you want to expose to Objective-C" 36 | severity: error -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "combine-schedulers", 6 | "repositoryURL": "https://github.com/pointfreeco/combine-schedulers", 7 | "state": { 8 | "branch": null, 9 | "revision": "ae2f434e81017bb7de02c168fb0bde83cd8370c1", 10 | "version": "0.3.1" 11 | } 12 | }, 13 | { 14 | "package": "swift-case-paths", 15 | "repositoryURL": "https://github.com/pointfreeco/swift-case-paths", 16 | "state": { 17 | "branch": null, 18 | "revision": "1aa1bf7c4069d9ba2f7edd36dbfc96ff1c58cbff", 19 | "version": "0.1.3" 20 | } 21 | }, 22 | { 23 | "package": "swift-composable-architecture", 24 | "repositoryURL": "https://github.com/pointfreeco/swift-composable-architecture", 25 | "state": { 26 | "branch": null, 27 | "revision": "86d6e3551437a88ecd73a789753fd2151761b17d", 28 | "version": "0.15.0" 29 | } 30 | } 31 | ] 32 | }, 33 | "version": 1 34 | } 35 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "swift-composable-kit", 8 | platforms: [ 9 | .iOS(.v13), 10 | .macOS(.v10_15), 11 | .tvOS(.v13), 12 | .watchOS(.v6), 13 | ], 14 | products: [ 15 | .library(name: "ComposableAltimeter", targets: ["ComposableAltimeter"]), 16 | .library(name: "ComposableAudioPlayer", targets: ["ComposableAudioPlayer"]), 17 | .library(name: "ComposableAudioRecorder", targets: ["ComposableAudioRecorder"]), 18 | .library(name: "ComposableBluetoothCentralManager", targets: ["ComposableBluetoothCentralManager"]), 19 | .library(name: "ComposableBluetoothPeripheralManager", targets: ["ComposableBluetoothPeripheralManager"]), 20 | .library(name: "ComposableCoreLocation", targets: ["ComposableCoreLocation"]), 21 | .library(name: "ComposableCoreMotion", targets: ["ComposableCoreMotion"]), 22 | .library(name: "ComposableFast", targets: ["ComposableFast"]), 23 | .library(name: "ComposableHealthStore", targets: ["ComposableHealthStore"]), 24 | .library(name: "ComposableLocalSearch", targets: ["ComposableLocalSearch"]), 25 | .library(name: "ComposableMediaPlayer", targets: ["ComposableMediaPlayer"]), 26 | .library(name: "ComposablePlayer", targets: ["ComposablePlayer"]), 27 | .library(name: "ComposableSpeechRecognizer", targets: ["ComposableSpeechRecognizer"]), 28 | .library(name: "ComposableSpeechSynthesizer", targets: ["ComposableSpeechSynthesizer"]), 29 | .library(name: "ComposableWatchConnectivity", targets: ["ComposableWatchConnectivity"]), 30 | .library(name: "ComposableWorkout", targets: ["ComposableWorkout"]), 31 | ], 32 | dependencies: [ 33 | .package(url: "https://github.com/pointfreeco/swift-composable-architecture", from: "0.15.0"), 34 | ], 35 | targets: [ 36 | .target( 37 | name: "ComposableAltimeter", 38 | dependencies: [ 39 | .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), 40 | ] 41 | ), 42 | .target( 43 | name: "ComposableAudioPlayer", 44 | dependencies: [ 45 | .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), 46 | ] 47 | ), 48 | .target( 49 | name: "ComposableAudioRecorder", 50 | dependencies: [ 51 | .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), 52 | ] 53 | ), 54 | .target( 55 | name: "ComposableBluetoothCentralManager", 56 | dependencies: [ 57 | .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), 58 | ] 59 | ), 60 | .target( 61 | name: "ComposableBluetoothPeripheralManager", 62 | dependencies: [ 63 | .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), 64 | ] 65 | ), 66 | .target( 67 | name: "ComposableCoreLocation", 68 | dependencies: [ 69 | .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), 70 | ] 71 | ), 72 | .target( 73 | name: "ComposableCoreMotion", 74 | dependencies: [ 75 | .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), 76 | ] 77 | ), 78 | .target( 79 | name: "ComposableFast", 80 | dependencies: [ 81 | .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), 82 | ] 83 | ), 84 | .target( 85 | name: "ComposableHealthStore", 86 | dependencies: [ 87 | .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), 88 | ] 89 | ), 90 | .target( 91 | name: "ComposableLocalSearch", 92 | dependencies: [ 93 | .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), 94 | ] 95 | ), 96 | .target( 97 | name: "ComposableMediaPlayer", 98 | dependencies: [ 99 | .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), 100 | ] 101 | ), 102 | .target( 103 | name: "ComposablePlayer", 104 | dependencies: [ 105 | .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), 106 | ] 107 | ), 108 | .target( 109 | name: "ComposableSpeechRecognizer", 110 | dependencies: [ 111 | .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), 112 | ] 113 | ), 114 | .target( 115 | name: "ComposableSpeechSynthesizer", 116 | dependencies: [ 117 | .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), 118 | ] 119 | ), 120 | .target( 121 | name: "ComposableWatchConnectivity", 122 | dependencies: [ 123 | .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), 124 | ] 125 | ), 126 | .target( 127 | name: "ComposableWorkout", 128 | dependencies: [ 129 | .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), 130 | ] 131 | ), 132 | .testTarget(name: "ComposableAudioPlayerTests", dependencies: ["ComposableAudioPlayer"]), 133 | .testTarget(name: "ComposableAudioRecorderTests", dependencies: ["ComposableAudioRecorder"]), 134 | .testTarget(name: "ComposableBluetoothCentralManagerTests", dependencies: ["ComposableBluetoothCentralManager"]), 135 | .testTarget(name: "ComposableBluetoothPeripheralManagerTests", dependencies: ["ComposableBluetoothPeripheralManager"]), 136 | .testTarget(name: "ComposableCoreLocationTests", dependencies: ["ComposableCoreLocation"]), 137 | .testTarget(name: "ComposableCoreMotionTests", dependencies: ["ComposableCoreMotion"]), 138 | .testTarget(name: "ComposableFastTests", dependencies: ["ComposableFast"]), 139 | .testTarget(name: "ComposablePlayerTests", dependencies: ["ComposablePlayer"]), 140 | .testTarget(name: "ComposableSpeechRecognizerTests", dependencies: ["ComposableSpeechRecognizer"]), 141 | .testTarget(name: "ComposableSpeechSynthesizerTests", dependencies: ["ComposableSpeechSynthesizer"]), 142 | ] 143 | ) 144 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwiftComposableKit 2 | 3 | [![CI](https://github.com/joeblau/swift-composable-kit/workflows/CI/badge.svg)](https://github.com/joeblau/swift-composable-kit/actions?query=workflow%3ACI) 4 | 5 | Composable implementation of Apple frameworks. To understand how the composable architecture works, I higly recommend watching the four part series on [Point Free](https://www.pointfree.co/collections/composable-architecture/a-tour-of-the-composable-architecture/ep100-a-tour-of-the-composable-architecture-part-1) 6 | 7 | 8 | ## Install 9 | 10 | Add this line to your `Package.swift` 11 | 12 | ```swift 13 | .package(url: "https://github.com/ridevelo/swift-composable-kit", from: "0.3.0"), 14 | ``` 15 | 16 | ## Libraries 17 | 18 | - **ComposableAltimeter** - CMAltimeter 19 | - **ComposableAudioPlayer** - AVAudioPlayer 20 | - **ComposableAudioRecorder** - AVAudioRecorder 21 | - **ComposableBluetoothCentralManager** - CBCentralManager 22 | - **ComposableBluetoothPeripheralManager** - CBPeripheralManager 23 | - **ComposableCoreLocation** - CLLocationManager 24 | - **ComposableCoreMotion** - CMMotionManager 25 | - **ComposableHealthStore** - HKHealthStore 26 | - **ComposableMediaPlayer** - MPMusicPlayerController 27 | - **[ComposableFast](./Sources/ComposableFast)** - https://fast.com 28 | - **ComposablePlayer** - AVPlayer 29 | - **ComposableSpeechRecognizer** - SFSpeechRecognizer 30 | - **ComposableSpeechSynthesizer** - AVSpeechSynthesizer 31 | - **ComposableWatchConnectivity** - WCSession 32 | - **ComposableWorkout** - HKWorkoutSession 33 | -------------------------------------------------------------------------------- /Sources/ComposableAltimeter/Interface.swift: -------------------------------------------------------------------------------- 1 | // Interface.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | // ComposableWorkoutInterface.swift 5 | // Copyright (c) 2020 Copilot 6 | #if canImport(CoreMotion) && (os(iOS) || os(watchOS)) 7 | import ComposableArchitecture 8 | import CoreMotion 9 | import Foundation 10 | 11 | public struct AltimeterManager { 12 | // MARK: - Variables 13 | 14 | var create: (AnyHashable) -> Effect = { _ in _unimplemented("create") } 15 | 16 | var destroy: (AnyHashable) -> Effect = { _ in _unimplemented("destroy") } 17 | 18 | public var authorizationStatus: () -> CMAuthorizationStatus = { _unimplemented("authorizationStatus") } 19 | public var isRelativeAltitudeAvailable: () -> Bool = { _unimplemented("isRelativeAltitudeAvailable") } 20 | 21 | var startRelativeAltitudeUpdates: (AnyHashable, OperationQueue) -> Effect = { _, _ in _unimplemented("startRelativeAltitudeUpdates") } 22 | var stopRelativeAltitudeUpdates: (AnyHashable) -> Effect = { _ in _unimplemented("stopRelativeAltitudeUpdates") } 23 | 24 | // MARK: - Functions 25 | 26 | public func create(id: AnyHashable) -> Effect { 27 | create(id) 28 | } 29 | 30 | public func destroy(id: AnyHashable) -> Effect { 31 | destroy(id) 32 | } 33 | 34 | public func startRelativeAltitudeUpdates(id: AnyHashable, to queue: OperationQueue = .main) -> Effect { 35 | startRelativeAltitudeUpdates(id, queue) 36 | } 37 | 38 | public func stopRelativeAltitudeUpdates(id: AnyHashable) -> Effect { 39 | stopRelativeAltitudeUpdates(id) 40 | } 41 | 42 | // public init( 43 | // create: @escaping (AnyHashable) -> Effect, 44 | // destroy: @escaping (AnyHashable) -> Effect, 45 | // authorizationStatus: @escaping () -> CMAuthorizationStatus, 46 | // isRelativeAltitudeAvailable: @escaping () -> Bool, 47 | // startRelativeAltitudeUpdates: @escaping (AnyHashable, OperationQueue) -> Effect, 48 | // stopRelativeAltitudeUpdates: @escaping (AnyHashable) -> Effect 49 | // ) { 50 | // self.create = create 51 | // self.destroy = destroy 52 | // self.authorizationStatus = authorizationStatus 53 | // self.isRelativeAltitudeAvailable = isRelativeAltitudeAvailable 54 | // self.startRelativeAltitudeUpdates = startRelativeAltitudeUpdates 55 | // self.stopRelativeAltitudeUpdates = stopRelativeAltitudeUpdates 56 | // } 57 | } 58 | #endif 59 | -------------------------------------------------------------------------------- /Sources/ComposableAltimeter/Live.swift: -------------------------------------------------------------------------------- 1 | // Live.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | #if canImport(CoreMotion) && (os(iOS) || os(watchOS)) 5 | import Combine 6 | import ComposableArchitecture 7 | import CoreMotion 8 | import Foundation 9 | 10 | extension AltimeterManager { 11 | public static let live = AltimeterManager( 12 | create: { id in 13 | .fireAndForget { 14 | if managers[id] != nil { 15 | return 16 | } 17 | managers[id] = CMAltimeter() 18 | } 19 | }, 20 | destroy: { id in 21 | .fireAndForget { managers[id] = nil } 22 | }, 23 | authorizationStatus: { 24 | CMAltimeter.authorizationStatus() 25 | }, 26 | isRelativeAltitudeAvailable: { 27 | CMAltimeter.isRelativeAltitudeAvailable() 28 | }, 29 | startRelativeAltitudeUpdates: { id, queue in 30 | Effect.run { subscriber in 31 | guard let manager = requireAltimeterManager(id: id) else { 32 | return AnyCancellable {} 33 | } 34 | 35 | guard deviceRelativeAltitudeUpdatesSubscribers[id] == nil else { 36 | return AnyCancellable {} 37 | } 38 | 39 | deviceRelativeAltitudeUpdatesSubscribers[id] = subscriber 40 | manager.startRelativeAltitudeUpdates(to: queue) { data, error in 41 | if let data = data { 42 | subscriber.send(.init(data)) 43 | } else if let error = error { 44 | subscriber.send(completion: .failure(error)) 45 | } 46 | } 47 | 48 | return AnyCancellable { 49 | manager.stopRelativeAltitudeUpdates() 50 | } 51 | } 52 | }, 53 | stopRelativeAltitudeUpdates: { id -> Effect in 54 | .fireAndForget { 55 | guard let manager = managers[id] 56 | else { 57 | couldNotFindAltimeterManager(id: id) 58 | return 59 | } 60 | manager.stopRelativeAltitudeUpdates() 61 | deviceRelativeAltitudeUpdatesSubscribers[id]?.send(completion: .finished) 62 | deviceRelativeAltitudeUpdatesSubscribers[id] = nil 63 | } 64 | } 65 | ) 66 | 67 | private static var managers: [AnyHashable: CMAltimeter] = [:] 68 | 69 | private static func requireAltimeterManager(id: AnyHashable) -> CMAltimeter? { 70 | if managers[id] == nil { 71 | couldNotFindAltimeterManager(id: id) 72 | } 73 | return managers[id] 74 | } 75 | } 76 | 77 | private var deviceRelativeAltitudeUpdatesSubscribers: [AnyHashable: Effect.Subscriber] = [:] 78 | 79 | private func couldNotFindAltimeterManager(id: Any) { 80 | assertionFailure( 81 | """ 82 | A altimeter manager could not be found with the id \(id). This is considered a programmer error. \ 83 | You should not invoke methods on a altimeter manager before it has been created or after it \ 84 | has been destroyed. Refactor your code to make sure there is a altimeter manager created by the \ 85 | time you invoke this endpoint. 86 | """) 87 | } 88 | 89 | #endif 90 | -------------------------------------------------------------------------------- /Sources/ComposableAltimeter/Mock.swift: -------------------------------------------------------------------------------- 1 | // Mock.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | // ComposableWorkoutMock.swift 5 | // Copyright (c) 2020 Copilot 6 | #if canImport(CoreMotion) && (os(iOS) || os(watchOS)) 7 | import ComposableArchitecture 8 | import CoreMotion 9 | 10 | public extension AltimeterManager { 11 | static func umimplemented( 12 | create: @escaping (AnyHashable) -> Effect = { _ in _unimplemented("create") }, 13 | destroy: @escaping (AnyHashable) -> Effect = { _ in _unimplemented("destroy") }, 14 | authorizationStatus: @escaping () -> CMAuthorizationStatus = { 15 | _unimplemented("authorizationStatus") 16 | }, 17 | isRelativeAltitudeAvailable: @escaping () -> Bool = { 18 | _unimplemented("isRelativeAltitudeAvailable") 19 | }, 20 | startRelativeAltitudeUpdates: @escaping (AnyHashable, OperationQueue) -> Effect = { _, _ in 21 | _unimplemented("startRelativeAltitudeUpdates") 22 | 23 | }, 24 | stopRelativeAltitudeUpdates: @escaping (AnyHashable) -> Effect = { _ in 25 | _unimplemented("stopRelativeAltitudeUpdates") 26 | } 27 | ) -> AltimeterManager { 28 | Self( 29 | create: create, 30 | destroy: destroy, 31 | authorizationStatus: authorizationStatus, 32 | isRelativeAltitudeAvailable: isRelativeAltitudeAvailable, 33 | startRelativeAltitudeUpdates: startRelativeAltitudeUpdates, 34 | stopRelativeAltitudeUpdates: stopRelativeAltitudeUpdates 35 | ) 36 | } 37 | } 38 | 39 | public func _unimplemented( 40 | _ function: StaticString, file: StaticString = #file, line: UInt = #line 41 | ) -> Never { 42 | fatalError( 43 | """ 44 | `\(function)` was called but is not implemented. Be sure to provide an implementation for 45 | this endpoint when creating the mock. 46 | """, 47 | file: file, 48 | line: line 49 | ) 50 | } 51 | #endif 52 | -------------------------------------------------------------------------------- /Sources/ComposableAltimeter/Models/RelativeAltitudeData.swift: -------------------------------------------------------------------------------- 1 | // RelativeAltitudeData.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | #if canImport(CoreMotion) && (os(iOS) || os(watchOS)) 5 | import CoreMotion 6 | 7 | /// Measurements of the Earth's magnetic field relative to the device. 8 | /// 9 | /// See the documentation for `CMMagnetometerData` for more info. 10 | public struct RelativeAltitudeData: Hashable, Equatable { 11 | public var relativeAltitude: NSNumber 12 | public var pressure: NSNumber 13 | 14 | public init(_ altitudeData: CMAltitudeData) { 15 | relativeAltitude = altitudeData.relativeAltitude 16 | pressure = altitudeData.pressure 17 | } 18 | 19 | public init( 20 | relativeAltitude: NSNumber, 21 | pressure: NSNumber 22 | ) { 23 | self.relativeAltitude = relativeAltitude 24 | self.pressure = pressure 25 | } 26 | 27 | public static func == (lhs: Self, rhs: Self) -> Bool { 28 | lhs.relativeAltitude == rhs.relativeAltitude 29 | && lhs.pressure == rhs.pressure 30 | } 31 | 32 | public func hash(into hasher: inout Hasher) { 33 | hasher.combine(relativeAltitude) 34 | hasher.combine(pressure) 35 | } 36 | } 37 | #endif 38 | -------------------------------------------------------------------------------- /Sources/ComposableAudioPlayer/Interface.swift: -------------------------------------------------------------------------------- 1 | // Interface.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | import AVFoundation 5 | import ComposableArchitecture 6 | 7 | public struct AudioPlayerManager { 8 | public enum Action: Equatable { 9 | case decodeErrorDidOccur(Error?) 10 | case didFinishPlaying(successfully: Bool) 11 | } 12 | 13 | public struct Error: Swift.Error, Equatable { 14 | public let error: NSError? 15 | 16 | public init(_ error: Swift.Error?) { 17 | self.error = error as NSError? 18 | } 19 | } 20 | 21 | // MARK: - Variables 22 | 23 | var create: (AnyHashable) -> Effect = { _ in _unimplemented("create") } 24 | 25 | var destroy: (AnyHashable) -> Effect = { _ in _unimplemented("destroy") } 26 | 27 | var play: (AnyHashable, URL) -> Effect = { _, _ in _unimplemented("play") } 28 | 29 | var stop: (AnyHashable) -> Effect = { _ in _unimplemented("stop") } 30 | 31 | // MARK: - Functions 32 | 33 | public func create(id: AnyHashable) -> Effect { 34 | create(id) 35 | } 36 | 37 | public func destroy(id: AnyHashable) -> Effect { 38 | destroy(id) 39 | } 40 | 41 | func play(id: AnyHashable, url: URL) -> Effect { 42 | play(id, url) 43 | } 44 | 45 | func stop(id: AnyHashable) -> Effect { 46 | stop(id) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/ComposableAudioPlayer/Live.swift: -------------------------------------------------------------------------------- 1 | // Live.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | import AVFoundation 5 | import Combine 6 | import ComposableArchitecture 7 | import Foundation 8 | 9 | public extension AudioPlayerManager { 10 | static let live: AudioPlayerManager = { () -> AudioPlayerManager in 11 | 12 | var manager = AudioPlayerManager() 13 | 14 | manager.create = { id in 15 | 16 | Effect.run { subscriber in 17 | let delegate = AudioPlayerManagerDelegate(subscriber) 18 | 19 | let player = AVAudioPlayer() 20 | player.delegate = delegate 21 | 22 | dependencies[id] = Dependencies( 23 | delegate: delegate, 24 | player: player, 25 | subscriber: subscriber, 26 | queue: OperationQueue.main 27 | ) 28 | 29 | return AnyCancellable { 30 | dependencies[id] = nil 31 | } 32 | } 33 | } 34 | 35 | manager.destroy = { id in 36 | .fireAndForget { 37 | dependencies[id]?.subscriber.send(completion: .finished) 38 | dependencies[id] = nil 39 | } 40 | } 41 | 42 | manager.play = { id, url in 43 | .fireAndForget { 44 | guard let player = try? AVAudioPlayer(contentsOf: url) else { return } 45 | dependencies[id]?.player = player 46 | dependencies[id]?.player.play() 47 | } 48 | } 49 | 50 | manager.stop = { id in 51 | .fireAndForget { dependencies[id]?.player.pause() } 52 | } 53 | 54 | return manager 55 | }() 56 | } 57 | 58 | private struct Dependencies { 59 | var delegate: AVAudioPlayerDelegate 60 | var player: AVAudioPlayer 61 | let subscriber: Effect.Subscriber 62 | let queue: OperationQueue 63 | } 64 | 65 | private var dependencies: [AnyHashable: Dependencies] = [:] 66 | 67 | private class AudioPlayerManagerDelegate: NSObject, AVAudioPlayerDelegate { 68 | let subscriber: Effect.Subscriber 69 | 70 | init(_ subscriber: Effect.Subscriber) { 71 | self.subscriber = subscriber 72 | } 73 | 74 | func audioPlayerDecodeErrorDidOccur(_: AVAudioPlayer, error: Error?) { 75 | subscriber.send(.decodeErrorDidOccur(AudioPlayerManager.Error(error))) 76 | } 77 | 78 | func audioPlayerDidFinishPlaying(_: AVAudioPlayer, successfully flag: Bool) { 79 | subscriber.send(.didFinishPlaying(successfully: flag)) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Sources/ComposableAudioPlayer/Mock.swift: -------------------------------------------------------------------------------- 1 | // Mock.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | import Foundation 5 | 6 | #if DEBUG 7 | public extension AudioPlayerManager { 8 | static func mock() -> Self { Self() } 9 | } 10 | #endif 11 | 12 | public func _unimplemented( 13 | _ function: StaticString, file: StaticString = #file, line: UInt = #line 14 | ) -> Never { 15 | fatalError( 16 | """ 17 | `\(function)` was called but is not implemented. Be sure to provide an implementation for 18 | this endpoint when creating the mock. 19 | """, 20 | file: file, 21 | line: line 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /Sources/ComposableAudioRecorder/Interface.swift: -------------------------------------------------------------------------------- 1 | // Interface.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | import Combine 5 | import ComposableArchitecture 6 | 7 | public struct AudioRecorderManager { 8 | public enum Action: Equatable { 9 | case encodeErrorDidOccur(Error?) 10 | case didFinishRecording(successfully: Bool) 11 | } 12 | 13 | public struct Error: Swift.Error, Equatable { 14 | public let error: NSError? 15 | 16 | public init(_ error: Swift.Error?) { 17 | self.error = error as NSError? 18 | } 19 | } 20 | 21 | // MARK: - Variables 22 | 23 | var create: (AnyHashable) -> Effect = { _ in _unimplemented("create") } 24 | 25 | var destroy: (AnyHashable) -> Effect = { _ in _unimplemented("destroy") } 26 | 27 | var record: (AnyHashable, URL, [String: Any]) -> Effect = { _, _, _ in _unimplemented("record") } 28 | 29 | var stop: (AnyHashable) -> Effect = { _ in _unimplemented("stop") } 30 | 31 | // MARK: - Functions 32 | 33 | public func create(id: AnyHashable) -> Effect { 34 | create(id) 35 | } 36 | 37 | public func destroy(id: AnyHashable) -> Effect { 38 | destroy(id) 39 | } 40 | 41 | func record(id: AnyHashable, url: URL, settings: [String: Any]) -> Effect { 42 | record(id, url, settings) 43 | } 44 | 45 | func stop(id: AnyHashable) -> Effect { 46 | stop(id) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/ComposableAudioRecorder/Live.swift: -------------------------------------------------------------------------------- 1 | // Live.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | import AVFoundation 5 | import Combine 6 | import ComposableArchitecture 7 | 8 | public extension AudioRecorderManager { 9 | static let live: AudioRecorderManager = { () -> AudioRecorderManager in 10 | 11 | var manager = AudioRecorderManager() 12 | 13 | manager.create = { id in 14 | 15 | Effect.run { subscriber in 16 | let recorder = AVAudioRecorder() 17 | var delegate = AudioRecorderDelegate(subscriber) 18 | recorder.delegate = delegate 19 | 20 | dependencies[id] = Dependencies( 21 | delegate: delegate, 22 | recorder: recorder, 23 | subscriber: subscriber, 24 | queue: OperationQueue.main 25 | ) 26 | 27 | return AnyCancellable { 28 | dependencies[id] = nil 29 | } 30 | } 31 | } 32 | 33 | manager.destroy = { id in 34 | .fireAndForget { 35 | dependencies[id]?.subscriber.send(completion: .finished) 36 | dependencies[id] = nil 37 | } 38 | } 39 | 40 | manager.record = { id, url, settings in 41 | .fireAndForget { 42 | do { 43 | let newRecorder = try AVAudioRecorder(url: url, settings: settings) 44 | newRecorder.delegate = dependencies[id]?.delegate 45 | dependencies[id]?.recorder = newRecorder 46 | dependencies[id]?.recorder.record() 47 | } catch { 48 | return 49 | } 50 | } 51 | } 52 | 53 | manager.stop = { id in 54 | .fireAndForget { 55 | guard dependencies[id]?.recorder != nil else { return } 56 | dependencies[id]?.recorder.stop() 57 | dependencies[id]?.recorder = nil 58 | } 59 | } 60 | 61 | return manager 62 | }() 63 | } 64 | 65 | private struct Dependencies { 66 | let delegate: AudioRecorderDelegate 67 | var recorder: AVAudioRecorder! 68 | let subscriber: Effect.Subscriber 69 | let queue: OperationQueue 70 | } 71 | 72 | private var dependencies: [AnyHashable: Dependencies] = [:] 73 | 74 | private class AudioRecorderDelegate: NSObject, AVAudioRecorderDelegate { 75 | let subscriber: Effect.Subscriber 76 | 77 | init(_ subscriber: Effect.Subscriber) { 78 | self.subscriber = subscriber 79 | } 80 | 81 | func audioRecorderEncodeErrorDidOccur(_: AVAudioRecorder, error: Error?) { 82 | subscriber.send(.encodeErrorDidOccur(AudioRecorderManager.Error(error))) 83 | } 84 | 85 | func audioRecorderDidFinishRecording(_: AVAudioRecorder, successfully flag: Bool) { 86 | subscriber.send(.didFinishRecording(successfully: flag)) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Sources/ComposableAudioRecorder/Mock.swift: -------------------------------------------------------------------------------- 1 | // Mock.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | import Foundation 5 | 6 | #if DEBUG 7 | public extension AudioRecorderManager { 8 | static func mock() -> Self { Self() } 9 | } 10 | 11 | #endif 12 | 13 | public func _unimplemented( 14 | _ function: StaticString, file: StaticString = #file, line: UInt = #line 15 | ) -> Never { 16 | fatalError( 17 | """ 18 | `\(function)` was called but is not implemented. Be sure to provide an implementation for 19 | this endpoint when creating the mock. 20 | """, 21 | file: file, 22 | line: line 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /Sources/ComposableBluetoothCentralManager/Extensions/BluetoothAdvertisementData.swift: -------------------------------------------------------------------------------- 1 | // BluetoothAdvertisementData.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | import CoreBluetooth 5 | import Foundation 6 | 7 | public struct BluetoothAdvertisementData: Equatable { 8 | public let localName: String? 9 | public let manufacturerData: Data? 10 | public let serviceData: [CBUUID: Data]? 11 | public let serviceUUIDs: [CBUUID]? 12 | public let overflowServiceUUIDs: [CBUUID]? 13 | public let powerLevel: NSNumber? 14 | public let isConnectable: Bool? 15 | public let solicitedServiceUUIDs: [CBUUID]? 16 | } 17 | 18 | extension BluetoothAdvertisementData { 19 | init(advertisementData: [String: Any]) { 20 | localName = advertisementData[CBAdvertisementDataLocalNameKey] as? String 21 | manufacturerData = advertisementData[CBAdvertisementDataLocalNameKey] as? Data 22 | serviceData = advertisementData[CBAdvertisementDataLocalNameKey] as? [CBUUID: Data] 23 | serviceUUIDs = advertisementData[CBAdvertisementDataLocalNameKey] as? [CBUUID] 24 | overflowServiceUUIDs = advertisementData[CBAdvertisementDataLocalNameKey] as? [CBUUID] 25 | powerLevel = advertisementData[CBAdvertisementDataLocalNameKey] as? NSNumber 26 | isConnectable = advertisementData[CBAdvertisementDataLocalNameKey] as? Bool 27 | solicitedServiceUUIDs = advertisementData[CBAdvertisementDataLocalNameKey] as? [CBUUID] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/ComposableBluetoothCentralManager/Extensions/PeripheralScanningOptions.swift: -------------------------------------------------------------------------------- 1 | // PeripheralScanningOptions.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | import CoreBluetooth 5 | import Foundation 6 | 7 | public struct PeripheralScanningOptions: Equatable { 8 | public let allowDuplicates: Bool? 9 | public let serviceUUIDs: [CBUUID]? 10 | } 11 | 12 | extension PeripheralScanningOptions { 13 | init(peripheralScanningOptions: [String: Any]?) { 14 | allowDuplicates = peripheralScanningOptions?[CBCentralManagerScanOptionAllowDuplicatesKey] as? Bool 15 | serviceUUIDs = peripheralScanningOptions?[CBCentralManagerScanOptionSolicitedServiceUUIDsKey] as? [CBUUID] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/ComposableBluetoothCentralManager/Extensions/StateRestorationOptions.swift: -------------------------------------------------------------------------------- 1 | // StateRestorationOptions.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | import CoreBluetooth 5 | import Foundation 6 | 7 | public struct StateRestorationOptions: Equatable { 8 | public let peripherals: [CBPeripheral]? 9 | public let scanServices: [CBUUID]? 10 | public let scanOptions: PeripheralScanningOptions? 11 | } 12 | 13 | extension StateRestorationOptions { 14 | init(restoreState: [String: Any]) { 15 | peripherals = restoreState[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral] 16 | scanServices = restoreState[CBCentralManagerRestoredStateScanServicesKey] as? [CBUUID] 17 | 18 | let options = restoreState[CBCentralManagerRestoredStateScanOptionsKey] as? [String: Any] 19 | scanOptions = PeripheralScanningOptions(peripheralScanningOptions: options) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/ComposableBluetoothCentralManager/Interface.swift: -------------------------------------------------------------------------------- 1 | // Interface.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | import Combine 5 | import ComposableArchitecture 6 | import CoreBluetooth 7 | 8 | public struct CentralManager { 9 | public enum Action: Equatable { 10 | case didConnect(peripheral: CBPeripheral) 11 | case didDisconnect(peripheral: CBPeripheral, error: Error?) 12 | case didFailToConnect(peripheral: CBPeripheral, error: Error?) 13 | #if os(iOS) || os(watchOS) || targetEnvironment(macCatalyst) 14 | case connectionEventDidOccur(event: CBConnectionEvent, peripheral: CBPeripheral) 15 | #endif 16 | 17 | case didDiscover(peripheral: CBPeripheral, advertisementData: BluetoothAdvertisementData, rssi: NSNumber) 18 | 19 | case centralManagerDidUpdateState 20 | case willRestore(state: StateRestorationOptions) 21 | 22 | case didUpdateANCSAuthorizationFor(peripheral: CBPeripheral) 23 | } 24 | 25 | var create: (AnyHashable, DispatchQueue?, [String: Any]?) -> Effect = { _, _, _ in _unimplemented("create") } 26 | 27 | var destroy: (AnyHashable) -> Effect = { _ in _unimplemented("destroy") } 28 | 29 | var connect: (AnyHashable, CBPeripheral, [String: Any]?) -> Effect = { _, _, _ in _unimplemented("connect") } 30 | 31 | var cancelPeripheralConnection: (AnyHashable, CBPeripheral) -> Effect = { _, _ in _unimplemented("cancelPeripheralConnection") } 32 | 33 | var retrieveConnectedPeripherals: (AnyHashable, [CBUUID]) -> [CBPeripheral]? = { _, _ in _unimplemented("retrieveConnectedPeripherals") } 34 | 35 | var retrievePeripherals: (AnyHashable, [UUID]) -> [CBPeripheral]? = { _, _ in _unimplemented("retrievePeripherals") } 36 | 37 | var scanForPeripherals: (AnyHashable, [CBUUID]?, [String: Any]?) -> Effect = { _, _, _ in _unimplemented("scanForPeripherals") } 38 | 39 | var stopScan: (AnyHashable) -> Effect = { _ in _unimplemented("stopScan") } 40 | 41 | public var isScanning: (AnyHashable) -> Bool = { _ in _unimplemented("isScanning") } 42 | 43 | public var state: (AnyHashable) -> CBManagerState = { _ in _unimplemented("state") } 44 | 45 | @available(macOS, unavailable) 46 | var supports: (AnyHashable, CBCentralManager.Feature) -> Bool = { _, _ in _unimplemented("supports") } 47 | 48 | var registerForConnectionEvents: (AnyHashable, [CBConnectionEventMatchingOption: Any]?) -> Effect = { _, _ in _unimplemented("registerForConnectionEvents") } 49 | 50 | public struct Error: Swift.Error, Equatable { 51 | public let error: NSError? 52 | 53 | public init(_ error: Swift.Error?) { 54 | self.error = error as NSError? 55 | } 56 | } 57 | 58 | // MARK: - Concrete 59 | 60 | public func create(id: AnyHashable, queue: DispatchQueue? = nil, options: [String: Any]? = nil) -> Effect { 61 | create(id, queue, options) 62 | } 63 | 64 | public func destroy(id: AnyHashable) -> Effect { 65 | destroy(id) 66 | } 67 | 68 | /// Establishing or Canceling Connections with Peripherals 69 | 70 | public func connect(id: AnyHashable, peripheral: CBPeripheral, options: [String: Any]?) -> Effect { 71 | connect(id, peripheral, options) 72 | } 73 | 74 | public func cancelPeripheralConnection(id: AnyHashable, peripheral: CBPeripheral) -> Effect { 75 | cancelPeripheralConnection(id, peripheral) 76 | } 77 | 78 | /// Retrieving Lists of Peripherals 79 | 80 | public func retrieveConnectedPeripherals(id: AnyHashable, withServices: [CBUUID]) -> [CBPeripheral]? { 81 | retrieveConnectedPeripherals(id, withServices) 82 | } 83 | 84 | public func retrievePeripherals(id: AnyHashable, withIdentifiers: [UUID]) -> [CBPeripheral]? { 85 | retrievePeripherals(id, withIdentifiers) 86 | } 87 | 88 | /// Scanning or Stopping Scans of Peripherals 89 | 90 | public func scanForPeripherals(id: AnyHashable, withServices: [CBUUID]?, options: [String: Any]?) -> Effect { 91 | scanForPeripherals(id, withServices, options) 92 | } 93 | 94 | public func stopScan(id: AnyHashable) -> Effect { 95 | stopScan(id) 96 | } 97 | 98 | public func isScanning(id: AnyHashable) -> Bool { 99 | isScanning(id) 100 | } 101 | 102 | public func state(id: AnyHashable) -> CBManagerState { 103 | state(id) 104 | } 105 | 106 | /// Inspecting Feature Support 107 | 108 | @available(macOS, unavailable) 109 | public func supports(id: AnyHashable, features: CBCentralManager.Feature) -> Bool { 110 | supports(id, features) 111 | } 112 | 113 | /// Receiving Connection Events 114 | 115 | public func registerForConnectionEvents(id: AnyHashable, options: [CBConnectionEventMatchingOption: Any]?) -> Effect { 116 | registerForConnectionEvents(id, options) 117 | } 118 | 119 | // MARK: - Actions 120 | } 121 | 122 | // MARK: - Properties 123 | 124 | public extension CentralManager { 125 | struct Properties: Equatable { 126 | public init() {} 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /Sources/ComposableBluetoothCentralManager/Live.swift: -------------------------------------------------------------------------------- 1 | // Live.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | import Combine 5 | import ComposableArchitecture 6 | import CoreBluetooth 7 | 8 | public extension CentralManager { 9 | static let live: CentralManager = { () -> CentralManager in 10 | var manager = CentralManager() 11 | 12 | manager.create = { id, queue, options in 13 | Effect.run { subscriber in 14 | var delegate = CentralManagerDelegate(subscriber) 15 | let manager = CBCentralManager(delegate: delegate, queue: queue, options: options) 16 | 17 | dependencies[id] = Dependencies(delegate: delegate, 18 | manager: manager, 19 | subscriber: subscriber) 20 | return AnyCancellable { 21 | dependencies[id] = nil 22 | } 23 | } 24 | } 25 | 26 | manager.destroy = { id in 27 | .fireAndForget { 28 | dependencies[id]?.subscriber.send(completion: .finished) 29 | dependencies[id] = nil 30 | } 31 | } 32 | 33 | manager.connect = { id, peripheral, options in 34 | .fireAndForget { 35 | dependencies[id]?.manager.connect(peripheral, options: options) 36 | } 37 | } 38 | 39 | manager.cancelPeripheralConnection = { id, peripheral in 40 | .fireAndForget { 41 | dependencies[id]?.manager.cancelPeripheralConnection(peripheral) 42 | } 43 | } 44 | 45 | manager.retrieveConnectedPeripherals = { id, services in 46 | dependencies[id]?.manager.retrieveConnectedPeripherals(withServices: services) 47 | } 48 | 49 | manager.retrievePeripherals = { id, identifiers in 50 | dependencies[id]?.manager.retrievePeripherals(withIdentifiers: identifiers) 51 | } 52 | 53 | manager.scanForPeripherals = { id, services, options in 54 | .fireAndForget { 55 | dependencies[id]?.manager.scanForPeripherals(withServices: services, options: options) 56 | } 57 | } 58 | 59 | manager.stopScan = { id in 60 | .fireAndForget { 61 | dependencies[id]?.manager.stopScan() 62 | } 63 | } 64 | 65 | #if os(iOS) || os(tvOS) || os(watchOS) || targetEnvironment(macCatalyst) 66 | manager.registerForConnectionEvents = { id, options in 67 | .fireAndForget { 68 | dependencies[id]?.manager.registerForConnectionEvents(options: options) 69 | } 70 | } 71 | #endif 72 | 73 | manager.state = { id in 74 | guard let dependency = dependencies[id] else { preconditionFailure("invalid dependency") } 75 | return dependency.manager.state 76 | } 77 | 78 | return manager 79 | }() 80 | } 81 | 82 | private struct Dependencies { 83 | let delegate: CBCentralManagerDelegate 84 | let manager: CBCentralManager 85 | let subscriber: Effect.Subscriber 86 | } 87 | 88 | private var dependencies: [AnyHashable: Dependencies] = [:] 89 | 90 | private class CentralManagerDelegate: NSObject, CBCentralManagerDelegate { 91 | let subscriber: Effect.Subscriber 92 | 93 | init(_ subscriber: Effect.Subscriber) { 94 | self.subscriber = subscriber 95 | } 96 | 97 | /// Monitoring Connections with Peripherals 98 | 99 | func centralManager(_: CBCentralManager, didConnect peripheral: CBPeripheral) { 100 | subscriber.send(.didConnect(peripheral: peripheral)) 101 | } 102 | 103 | func centralManager(_: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { 104 | subscriber.send(.didDisconnect(peripheral: peripheral, error: CentralManager.Error(error))) 105 | } 106 | 107 | func centralManager(_: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { 108 | subscriber.send(.didFailToConnect(peripheral: peripheral, error: CentralManager.Error(error))) 109 | } 110 | 111 | #if os(iOS) || os(watchOS) || targetEnvironment(macCatalyst) 112 | func centralManager(_: CBCentralManager, connectionEventDidOccur event: CBConnectionEvent, for peripheral: CBPeripheral) { 113 | subscriber.send(.connectionEventDidOccur(event: event, peripheral: peripheral)) 114 | } 115 | #endif 116 | 117 | /// Discovering and Retrieving Peripherals 118 | 119 | func centralManager(_: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) { 120 | let data = BluetoothAdvertisementData(advertisementData: advertisementData) 121 | subscriber.send(.didDiscover(peripheral: peripheral, advertisementData: data, rssi: RSSI)) 122 | } 123 | 124 | /// Monitoring the Central Manager’s State 125 | 126 | func centralManagerDidUpdateState(_: CBCentralManager) { 127 | subscriber.send(.centralManagerDidUpdateState) 128 | } 129 | 130 | func centralManager(_: CBCentralManager, willRestoreState dict: [String: Any]) { 131 | let data = StateRestorationOptions(restoreState: dict) 132 | subscriber.send(.willRestore(state: data)) 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /Sources/ComposableBluetoothCentralManager/Mock.swift: -------------------------------------------------------------------------------- 1 | // Mock.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | import ComposableArchitecture 5 | import CoreBluetooth 6 | 7 | #if DEBUG 8 | public extension CentralManager { 9 | static func mock() -> Self { 10 | Self() 11 | } 12 | } 13 | 14 | #endif 15 | 16 | public func _unimplemented( 17 | _ function: StaticString, file: StaticString = #file, line: UInt = #line 18 | ) -> Never { 19 | fatalError( 20 | """ 21 | `\(function)` was called but is not implemented. Be sure to provide an implementation for 22 | this endpoint when creating the mock. 23 | """, 24 | file: file, 25 | line: line 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /Sources/ComposableBluetoothPeripheralManager/Interface.swift: -------------------------------------------------------------------------------- 1 | // Interface.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | import Combine 5 | import ComposableArchitecture 6 | import CoreBluetooth 7 | 8 | public struct PeripheralManager { 9 | public enum Action: Equatable { 10 | /// Discovering Services 11 | case didDiscoverServices(_ peripheral: CBPeripheral, error: Error?) 12 | case didDiscoverIncludedServicesFor(_ peripheral: CBPeripheral, service: CBService, error: Error?) 13 | 14 | /// Discovering Characteristics and their Descriptors 15 | case didDiscoverCharacteristicsFor(_ peripheral: CBPeripheral, service: CBService, error: Error?) 16 | case didDiscoverDescriptorsFor(_ peripheral: CBPeripheral, characteristic: CBCharacteristic, error: Error?) 17 | 18 | /// Retrieving Characteristic and Descriptor Values 19 | case didUpdateCharacteristicValueFor(_ peripheral: CBPeripheral, characteristic: CBCharacteristic, error: Error?) 20 | case didUpdateDescriptorValueFor(_ peripheral: CBPeripheral, descriptor: CBDescriptor, error: Error?) 21 | 22 | /// Writing Characteristic and Descriptor Values 23 | case didWriteCharacteristicValueFor(_ peripheral: CBPeripheral, characteristic: CBCharacteristic, error: Error?) 24 | case didWriteDescriptorValueFor(_ peripheral: CBPeripheral, descriptor: CBDescriptor, error: Error?) 25 | case peripheralIsReadyToSendWriteWithoutResponse(peripheral: CBPeripheral) 26 | 27 | /// Managing Notifications for a Characteristic’s Value 28 | case didUpdateNotificationStateFor(_ peripheral: CBPeripheral, characteristic: CBCharacteristic, error: Error?) 29 | 30 | /// Retrieving a Peripheral’s RSSI Data 31 | case didReadRSSI(_ peripheral: CBPeripheral, rssi: NSNumber, error: Error?) 32 | 33 | /// Monitoring Changes to a Peripheral’s Name or Services 34 | 35 | case peripheralDidUpdateName(_ peripheral: CBPeripheral) 36 | case didModifyServices(_ peripheral: CBPeripheral, services: [CBService]) 37 | 38 | /// Monitoring L2CAP Channels 39 | case didOpen(_ peripheral: CBPeripheral, channel: CBL2CAPChannel?, error: Error?) 40 | } 41 | 42 | var create: (AnyHashable) -> Effect = { _ in _unimplemented("create") } 43 | 44 | var destroy: (AnyHashable) -> Effect = { _ in _unimplemented("destroy") } 45 | 46 | var addPeripheral: (AnyHashable, CBPeripheral, [CBUUID]?) -> Effect = { _, _, _ in _unimplemented("addPeripheral") } 47 | 48 | public struct Error: Swift.Error, Equatable { 49 | public let error: NSError? 50 | 51 | public init(_ error: Swift.Error?) { 52 | self.error = error as NSError? 53 | } 54 | } 55 | 56 | // MARK: - Concrete 57 | 58 | public func create(id: AnyHashable) -> Effect { 59 | create(id) 60 | } 61 | 62 | public func destroy(id: AnyHashable) -> Effect { 63 | destroy(id) 64 | } 65 | 66 | public func addPeriphal(id: AnyHashable, peripheral: CBPeripheral, services: [CBUUID]?) -> Effect { 67 | addPeripheral(id, peripheral, services) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Sources/ComposableBluetoothPeripheralManager/Live.swift: -------------------------------------------------------------------------------- 1 | // Live.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | import Combine 5 | import ComposableArchitecture 6 | import CoreBluetooth 7 | 8 | public extension PeripheralManager { 9 | static let live: PeripheralManager = { () -> PeripheralManager in 10 | var peripheral = PeripheralManager() 11 | 12 | peripheral.create = { id in 13 | Effect.run { subscriber in 14 | var delegate = PeripheralDelegate(subscriber) 15 | let peripherals = [CBPeripheral]() 16 | 17 | dependencies[id] = Dependencies(delegate: delegate, 18 | peripherals: peripherals, 19 | subscriber: subscriber) 20 | return AnyCancellable { 21 | dependencies[id] = nil 22 | } 23 | } 24 | } 25 | 26 | peripheral.destroy = { id in 27 | .fireAndForget { 28 | dependencies[id]?.subscriber.send(completion: .finished) 29 | dependencies[id] = nil 30 | } 31 | } 32 | 33 | peripheral.addPeripheral = { id, peripheral, services in 34 | .fireAndForget { 35 | dependencies[id]?.peripherals.append(peripheral) 36 | peripheral.delegate = dependencies[id]?.delegate 37 | peripheral.discoverServices(services) 38 | } 39 | } 40 | 41 | return peripheral 42 | }() 43 | } 44 | 45 | private struct Dependencies { 46 | let delegate: CBPeripheralDelegate 47 | var peripherals: [CBPeripheral] 48 | let subscriber: Effect.Subscriber 49 | } 50 | 51 | private var dependencies: [AnyHashable: Dependencies] = [:] 52 | 53 | private class PeripheralDelegate: NSObject, CBPeripheralDelegate { 54 | let subscriber: Effect.Subscriber 55 | 56 | init(_ subscriber: Effect.Subscriber) { 57 | self.subscriber = subscriber 58 | } 59 | 60 | /// Discovering Services 61 | 62 | func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { 63 | subscriber.send(.didDiscoverServices(peripheral, error: PeripheralManager.Error(error))) 64 | } 65 | 66 | func peripheral(_ peripheral: CBPeripheral, didDiscoverIncludedServicesFor service: CBService, error: Error?) { 67 | subscriber.send(.didDiscoverIncludedServicesFor(peripheral, service: service, error: PeripheralManager.Error(error))) 68 | } 69 | 70 | /// Discovering Characteristics and their Descriptors 71 | 72 | func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { 73 | subscriber.send(.didDiscoverCharacteristicsFor(peripheral, service: service, error: PeripheralManager.Error(error))) 74 | } 75 | 76 | func peripheral(_ peripheral: CBPeripheral, didDiscoverDescriptorsFor characteristic: CBCharacteristic, error: Error?) { 77 | subscriber.send(.didDiscoverDescriptorsFor(peripheral, characteristic: characteristic, error: PeripheralManager.Error(error))) 78 | } 79 | 80 | /// Retrieving Characteristic and Descriptor Values 81 | 82 | func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { 83 | subscriber.send(.didUpdateCharacteristicValueFor(peripheral, characteristic: characteristic, error: PeripheralManager.Error(error))) 84 | } 85 | 86 | func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor descriptor: CBDescriptor, error: Error?) { 87 | subscriber.send(.didUpdateDescriptorValueFor(peripheral, descriptor: descriptor, error: PeripheralManager.Error(error))) 88 | } 89 | 90 | /// Writing Characteristic and Descriptor Values 91 | 92 | func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) { 93 | subscriber.send(.didWriteCharacteristicValueFor(peripheral, characteristic: characteristic, error: PeripheralManager.Error(error))) 94 | } 95 | 96 | func peripheral(_ peripheral: CBPeripheral, didWriteValueFor descriptor: CBDescriptor, error: Error?) { 97 | subscriber.send(.didWriteDescriptorValueFor(peripheral, descriptor: descriptor, error: PeripheralManager.Error(error))) 98 | } 99 | 100 | func peripheralIsReady(toSendWriteWithoutResponse peripheral: CBPeripheral) { 101 | subscriber.send(.peripheralIsReadyToSendWriteWithoutResponse(peripheral: peripheral)) 102 | } 103 | 104 | /// Managing Notifications for a Characteristic’s Value 105 | 106 | func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) { 107 | subscriber.send(.didUpdateNotificationStateFor(peripheral, characteristic: characteristic, error: PeripheralManager.Error(error))) 108 | } 109 | 110 | /// Retrieving a Peripheral’s RSSI Data 111 | 112 | func peripheral(_ peripheral: CBPeripheral, didReadRSSI RSSI: NSNumber, error: Error?) { 113 | subscriber.send(.didReadRSSI(peripheral, rssi: RSSI, error: PeripheralManager.Error(error))) 114 | } 115 | 116 | /// Monitoring Changes to a Peripheral’s Name or Services 117 | 118 | func peripheralDidUpdateName(_ peripheral: CBPeripheral) { 119 | subscriber.send(.peripheralDidUpdateName(peripheral)) 120 | } 121 | 122 | func peripheral(_ peripheral: CBPeripheral, didModifyServices invalidatedServices: [CBService]) { 123 | subscriber.send(.didModifyServices(peripheral, services: invalidatedServices)) 124 | } 125 | 126 | /// Monitoring L2CAP Channels 127 | 128 | func peripheral(_ peripheral: CBPeripheral, didOpen channel: CBL2CAPChannel?, error: Error?) { 129 | subscriber.send(.didOpen(peripheral, channel: channel, error: PeripheralManager.Error(error))) 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /Sources/ComposableBluetoothPeripheralManager/Mock.swift: -------------------------------------------------------------------------------- 1 | // Mock.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | import ComposableArchitecture 5 | import CoreBluetooth 6 | 7 | #if DEBUG 8 | public extension PeripheralManager { 9 | static func mock() -> Self { 10 | Self() 11 | } 12 | } 13 | 14 | #endif 15 | 16 | public func _unimplemented( 17 | _ function: StaticString, file: StaticString = #file, line: UInt = #line 18 | ) -> Never { 19 | fatalError( 20 | """ 21 | `\(function)` was called but is not implemented. Be sure to provide an implementation for 22 | this endpoint when creating the mock. 23 | """, 24 | file: file, 25 | line: line 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /Sources/ComposableCoreLocation/Internal/Exports.swift: -------------------------------------------------------------------------------- 1 | // Exports.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | @_exported import ComposableArchitecture 5 | @_exported import CoreLocation 6 | -------------------------------------------------------------------------------- /Sources/ComposableCoreLocation/Live.swift: -------------------------------------------------------------------------------- 1 | // Live.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | import Combine 5 | import ComposableArchitecture 6 | import CoreLocation 7 | 8 | public extension LocationManager { 9 | /// The live implementation of the `LocationManager` interface. This implementation is capable of 10 | /// creating real `CLLocationManager` instances, listening to its delegate methods, and invoking 11 | /// its methods. You will typically use this when building for the simulator or device: 12 | /// 13 | /// let store = Store( 14 | /// initialState: AppState(), 15 | /// reducer: appReducer, 16 | /// environment: AppEnvironment( 17 | /// locationManager: LocationManager.live 18 | /// ) 19 | /// ) 20 | /// 21 | static let live: LocationManager = { () -> LocationManager in 22 | var manager = LocationManager() 23 | 24 | manager.authorizationStatus = CLLocationManager.authorizationStatus 25 | 26 | manager.create = { id in 27 | Effect.run { subscriber in 28 | let manager = CLLocationManager() 29 | var delegate = LocationManagerDelegate(subscriber) 30 | manager.delegate = delegate 31 | 32 | dependencies[id] = Dependencies( 33 | delegate: delegate, 34 | manager: manager, 35 | subscriber: subscriber 36 | ) 37 | 38 | return AnyCancellable { 39 | dependencies[id] = nil 40 | } 41 | } 42 | } 43 | 44 | manager.destroy = { id in 45 | .fireAndForget { 46 | dependencies[id]?.subscriber.send(completion: .finished) 47 | dependencies[id] = nil 48 | } 49 | } 50 | 51 | manager.locationServicesEnabled = CLLocationManager.locationServicesEnabled 52 | 53 | manager.location = { id in dependencies[id]?.manager.location.map(Location.init(rawValue:)) } 54 | 55 | manager.accuracyAuthorization = { id in 56 | #if (compiler(>=5.3) && !(os(macOS) || targetEnvironment(macCatalyst))) || compiler(>=5.3.1) 57 | if #available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, macCatalyst 14.0, *) { 58 | return AccuracyAuthorization(dependencies[id]?.manager.accuracyAuthorization) 59 | } 60 | #endif 61 | return nil 62 | } 63 | 64 | manager.requestLocation = { id in 65 | .fireAndForget { dependencies[id]?.manager.requestLocation() } 66 | } 67 | 68 | #if (os(iOS) && !APPCLIP) || os(macOS) || os(watchOS) || targetEnvironment(macCatalyst) 69 | manager.requestAlwaysAuthorization = { id in 70 | .fireAndForget { dependencies[id]?.manager.requestAlwaysAuthorization() } 71 | } 72 | #endif 73 | 74 | #if os(iOS) || os(tvOS) || os(watchOS) || targetEnvironment(macCatalyst) 75 | manager.requestWhenInUseAuthorization = { id in 76 | .fireAndForget { dependencies[id]?.manager.requestWhenInUseAuthorization() } 77 | } 78 | #endif 79 | 80 | manager.set = { id, properties in 81 | .fireAndForget { 82 | guard let manager = dependencies[id]?.manager else { return } 83 | 84 | #if os(iOS) || os(watchOS) || targetEnvironment(macCatalyst) 85 | if let activityType = properties.activityType { 86 | manager.activityType = activityType 87 | } 88 | if let allowsBackgroundLocationUpdates = properties.allowsBackgroundLocationUpdates { 89 | manager.allowsBackgroundLocationUpdates = allowsBackgroundLocationUpdates 90 | } 91 | #endif 92 | #if os(iOS) || os(macOS) || os(tvOS) || os(watchOS) || targetEnvironment(macCatalyst) 93 | if let desiredAccuracy = properties.desiredAccuracy { 94 | manager.desiredAccuracy = desiredAccuracy 95 | } 96 | if let distanceFilter = properties.distanceFilter { 97 | manager.distanceFilter = distanceFilter 98 | } 99 | #endif 100 | #if os(iOS) || os(watchOS) || targetEnvironment(macCatalyst) 101 | if let headingFilter = properties.headingFilter { 102 | manager.headingFilter = headingFilter 103 | } 104 | if let headingOrientation = properties.headingOrientation { 105 | manager.headingOrientation = headingOrientation 106 | } 107 | #endif 108 | #if os(iOS) || targetEnvironment(macCatalyst) 109 | if let pausesLocationUpdatesAutomatically = properties.pausesLocationUpdatesAutomatically 110 | { 111 | manager.pausesLocationUpdatesAutomatically = pausesLocationUpdatesAutomatically 112 | } 113 | if let showsBackgroundLocationIndicator = properties.showsBackgroundLocationIndicator { 114 | manager.showsBackgroundLocationIndicator = showsBackgroundLocationIndicator 115 | } 116 | #endif 117 | } 118 | } 119 | 120 | #if os(iOS) || targetEnvironment(macCatalyst) 121 | manager.startMonitoringVisits = { id in 122 | .fireAndForget { dependencies[id]?.manager.startMonitoringVisits() } 123 | } 124 | #endif 125 | 126 | #if os(iOS) || os(macOS) || os(watchOS) || targetEnvironment(macCatalyst) 127 | manager.startUpdatingLocation = { id in 128 | .fireAndForget { dependencies[id]?.manager.startUpdatingLocation() } 129 | } 130 | #endif 131 | 132 | #if os(iOS) || targetEnvironment(macCatalyst) 133 | manager.stopMonitoringVisits = { id in 134 | .fireAndForget { dependencies[id]?.manager.stopMonitoringVisits() } 135 | } 136 | #endif 137 | 138 | #if os(iOS) || os(watchOS) || targetEnvironment(macCatalyst) 139 | manager.startUpdatingHeading = { id in 140 | .fireAndForget { dependencies[id]?.manager.startUpdatingHeading() } 141 | } 142 | #endif 143 | 144 | #if os(iOS) || os(watchOS) || targetEnvironment(macCatalyst) 145 | manager.stopUpdatingHeading = { id in 146 | .fireAndForget { dependencies[id]?.manager.stopUpdatingHeading() } 147 | } 148 | #endif 149 | 150 | manager.stopUpdatingLocation = { id in 151 | .fireAndForget { dependencies[id]?.manager.stopUpdatingLocation() } 152 | } 153 | 154 | return manager 155 | }() 156 | } 157 | 158 | private struct Dependencies { 159 | let delegate: LocationManagerDelegate 160 | let manager: CLLocationManager 161 | let subscriber: Effect.Subscriber 162 | } 163 | 164 | private var dependencies: [AnyHashable: Dependencies] = [:] 165 | 166 | private class LocationManagerDelegate: NSObject, CLLocationManagerDelegate { 167 | let subscriber: Effect.Subscriber 168 | 169 | init(_ subscriber: Effect.Subscriber) { 170 | self.subscriber = subscriber 171 | } 172 | 173 | func locationManager( 174 | _: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus 175 | ) { 176 | subscriber.send(.didChangeAuthorization(status)) 177 | } 178 | 179 | func locationManager(_: CLLocationManager, didFailWithError error: Error) { 180 | subscriber.send(.didFailWithError(LocationManager.Error(error))) 181 | } 182 | 183 | func locationManager(_: CLLocationManager, didUpdateLocations locations: [CLLocation]) { 184 | subscriber.send(.didUpdateLocations(locations.map(Location.init(rawValue:)))) 185 | } 186 | 187 | #if os(macOS) 188 | func locationManager( 189 | _: CLLocationManager, didUpdateTo newLocation: CLLocation, 190 | from oldLocation: CLLocation 191 | ) { 192 | subscriber.send( 193 | .didUpdateTo( 194 | newLocation: Location(rawValue: newLocation), 195 | oldLocation: Location(rawValue: oldLocation) 196 | ) 197 | ) 198 | } 199 | #endif 200 | 201 | #if os(iOS) || os(macOS) || targetEnvironment(macCatalyst) 202 | func locationManager( 203 | _: CLLocationManager, didFinishDeferredUpdatesWithError error: Error? 204 | ) { 205 | subscriber.send(.didFinishDeferredUpdatesWithError(error.map(LocationManager.Error.init))) 206 | } 207 | #endif 208 | 209 | #if os(iOS) || targetEnvironment(macCatalyst) 210 | func locationManagerDidPauseLocationUpdates(_: CLLocationManager) { 211 | subscriber.send(.didPauseLocationUpdates) 212 | } 213 | #endif 214 | 215 | #if os(iOS) || targetEnvironment(macCatalyst) 216 | func locationManagerDidResumeLocationUpdates(_: CLLocationManager) { 217 | subscriber.send(.didResumeLocationUpdates) 218 | } 219 | #endif 220 | 221 | #if os(iOS) || os(watchOS) || targetEnvironment(macCatalyst) 222 | func locationManager(_: CLLocationManager, didUpdateHeading newHeading: CLHeading) { 223 | subscriber.send(.didUpdateHeading(newHeading: Heading(rawValue: newHeading))) 224 | } 225 | #endif 226 | 227 | #if os(iOS) || os(macOS) || targetEnvironment(macCatalyst) 228 | func locationManager(_: CLLocationManager, didEnterRegion region: CLRegion) { 229 | subscriber.send(.didEnterRegion(Region(rawValue: region))) 230 | } 231 | #endif 232 | 233 | #if os(iOS) || os(macOS) || targetEnvironment(macCatalyst) 234 | func locationManager(_: CLLocationManager, didExitRegion region: CLRegion) { 235 | subscriber.send(.didExitRegion(Region(rawValue: region))) 236 | } 237 | #endif 238 | 239 | #if os(iOS) || os(macOS) || targetEnvironment(macCatalyst) 240 | func locationManager( 241 | _: CLLocationManager, didDetermineState state: CLRegionState, for region: CLRegion 242 | ) { 243 | subscriber.send(.didDetermineState(state, region: Region(rawValue: region))) 244 | } 245 | #endif 246 | 247 | #if os(iOS) || os(macOS) || targetEnvironment(macCatalyst) 248 | func locationManager( 249 | _: CLLocationManager, monitoringDidFailFor region: CLRegion?, withError error: Error 250 | ) { 251 | subscriber.send( 252 | .monitoringDidFail( 253 | region: region.map(Region.init(rawValue:)), error: LocationManager.Error(error) 254 | )) 255 | } 256 | #endif 257 | 258 | #if os(iOS) || os(macOS) || targetEnvironment(macCatalyst) 259 | func locationManager(_: CLLocationManager, didStartMonitoringFor region: CLRegion) { 260 | subscriber.send(.didStartMonitoring(region: Region(rawValue: region))) 261 | } 262 | #endif 263 | 264 | #if os(iOS) || targetEnvironment(macCatalyst) 265 | func locationManager( 266 | _: CLLocationManager, didRange beacons: [CLBeacon], 267 | satisfying beaconConstraint: CLBeaconIdentityConstraint 268 | ) { 269 | subscriber.send( 270 | .didRangeBeacons( 271 | beacons.map(Beacon.init(rawValue:)), satisfyingConstraint: beaconConstraint 272 | )) 273 | } 274 | #endif 275 | 276 | #if os(iOS) || targetEnvironment(macCatalyst) 277 | func locationManager( 278 | _: CLLocationManager, didFailRangingFor beaconConstraint: CLBeaconIdentityConstraint, 279 | error: Error 280 | ) { 281 | subscriber.send( 282 | .didFailRanging(beaconConstraint: beaconConstraint, error: LocationManager.Error(error))) 283 | } 284 | #endif 285 | 286 | #if os(iOS) || targetEnvironment(macCatalyst) 287 | func locationManager(_: CLLocationManager, didVisit visit: CLVisit) { 288 | subscriber.send(.didVisit(Visit(visit: visit))) 289 | } 290 | #endif 291 | } 292 | -------------------------------------------------------------------------------- /Sources/ComposableCoreLocation/Mock.swift: -------------------------------------------------------------------------------- 1 | // Mock.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | #if DEBUG 5 | import ComposableArchitecture 6 | import CoreLocation 7 | 8 | public extension LocationManager { 9 | /// The mock implementation of the `LocationManager` interface. By default this implementation 10 | /// stubs all of its endpoints as functions that immediately `fatalError`. So, to construct a 11 | /// mock you will invoke the `.unimplemented` static method, and provide implementations for all 12 | /// of the endpoints that you expect your test to need access to. 13 | /// 14 | /// This allows you to test an even deeper property of your features: that they use only the 15 | /// location manager endpoints that you specify and nothing else. This can be useful as a 16 | /// measurement of just how complex a particular test is. Tests that need to stub many endpoints 17 | /// are in some sense more complicated than tests that only need to stub a few endpoints. It's 18 | /// not necessarily a bad thing to stub many endpoints. Sometimes it's needed. 19 | /// 20 | /// As an example, to create a mock manager that simulates a location manager that has already 21 | /// authorized access to location, and when a location is requested it immediately responds 22 | /// with a mock location we can do something like this: 23 | /// 24 | /// // Send actions to this subject to simulate the location manager's delegate methods 25 | /// // being called. 26 | /// let locationManagerSubject = PassthroughSubject() 27 | /// 28 | /// // The mock location we want the manager to say we are located at 29 | /// let mockLocation = Location( 30 | /// coordinate: CLLocationCoordinate2D(latitude: 40.6501, longitude: -73.94958), 31 | /// // A whole bunch of other properties have been omitted. 32 | /// ) 33 | /// 34 | /// let manager = LocationManager.unimplemented( 35 | /// // Override any CLLocationManager endpoints your test invokes: 36 | /// 37 | /// authorizationStatus: { .authorizedAlways }, 38 | /// create: { _ in locationManagerSubject.eraseToEffect() }, 39 | /// locationServicesEnabled: { true }, 40 | /// requestLocation: { _ in 41 | /// .fireAndForget { locationManagerSubject.send(.didUpdateLocations([mockLocation])) } 42 | /// } 43 | /// ) 44 | /// 45 | static func unimplemented( 46 | accuracyAuthorization: @escaping (AnyHashable) -> AccuracyAuthorization? = { _ in 47 | _unimplemented("accuracyAuthorization") 48 | }, 49 | authorizationStatus: @escaping () -> CLAuthorizationStatus = { 50 | _unimplemented("authorizationStatus") 51 | }, 52 | create: @escaping (_ id: AnyHashable) -> Effect = { _ in 53 | _unimplemented("create") 54 | }, 55 | destroy: @escaping (AnyHashable) -> Effect = { _ in _unimplemented("destroy") }, 56 | dismissHeadingCalibrationDisplay: @escaping (AnyHashable) -> Effect = { _ in 57 | _unimplemented("dismissHeadingCalibrationDisplay") 58 | }, 59 | heading: @escaping (AnyHashable) -> Heading? = { _ in _unimplemented("heading") }, 60 | headingAvailable: @escaping () -> Bool = { _unimplemented("headingAvailable") }, 61 | isRangingAvailable: @escaping () -> Bool = { _unimplemented("isRangingAvailable") }, 62 | location: @escaping (AnyHashable) -> Location? = { _ in _unimplemented("location") }, 63 | locationServicesEnabled: @escaping () -> Bool = { _unimplemented("locationServicesEnabled") }, 64 | maximumRegionMonitoringDistance: @escaping (AnyHashable) -> CLLocationDistance = { _ in 65 | _unimplemented("maximumRegionMonitoringDistance") 66 | }, 67 | monitoredRegions: @escaping (AnyHashable) -> Set = { _ in 68 | _unimplemented("monitoredRegions") 69 | }, 70 | requestAlwaysAuthorization: @escaping (AnyHashable) -> Effect = { _ in 71 | _unimplemented("requestAlwaysAuthorization") 72 | }, 73 | requestLocation: @escaping (AnyHashable) -> Effect = { _ in 74 | _unimplemented("requestLocation") 75 | }, 76 | requestWhenInUseAuthorization: @escaping (AnyHashable) -> Effect = { _ in 77 | _unimplemented("requestWhenInUseAuthorization") 78 | }, 79 | set: @escaping (_ id: AnyHashable, _ properties: Properties) -> Effect = { 80 | _, _ in _unimplemented("set") 81 | }, 82 | significantLocationChangeMonitoringAvailable: @escaping () -> Bool = { 83 | _unimplemented("significantLocationChangeMonitoringAvailable") 84 | }, 85 | startMonitoringSignificantLocationChanges: @escaping (AnyHashable) -> Effect = { 86 | _ in _unimplemented("startMonitoringSignificantLocationChanges") 87 | }, 88 | startMonitoringForRegion: @escaping (AnyHashable, Region) -> Effect = { _, _ in 89 | _unimplemented("startMonitoringForRegion") 90 | }, 91 | startMonitoringVisits: @escaping (AnyHashable) -> Effect = { _ in 92 | _unimplemented("startMonitoringVisits") 93 | }, 94 | startUpdatingLocation: @escaping (AnyHashable) -> Effect = { _ in 95 | _unimplemented("startUpdatingLocation") 96 | }, 97 | stopMonitoringSignificantLocationChanges: @escaping (AnyHashable) -> Effect = { 98 | _ in _unimplemented("stopMonitoringSignificantLocationChanges") 99 | }, 100 | stopMonitoringForRegion: @escaping (AnyHashable, Region) -> Effect = { _, _ in 101 | _unimplemented("stopMonitoringForRegion") 102 | }, 103 | stopMonitoringVisits: @escaping (AnyHashable) -> Effect = { _ in 104 | _unimplemented("stopMonitoringVisits") 105 | }, 106 | startUpdatingHeading: @escaping (AnyHashable) -> Effect = { _ in 107 | _unimplemented("startUpdatingHeading") 108 | }, 109 | stopUpdatingHeading: @escaping (AnyHashable) -> Effect = { _ in 110 | _unimplemented("stopUpdatingHeading") 111 | }, 112 | stopUpdatingLocation: @escaping (AnyHashable) -> Effect = { _ in 113 | _unimplemented("stopUpdatingLocation") 114 | } 115 | ) -> Self { 116 | Self( 117 | accuracyAuthorization: accuracyAuthorization, 118 | authorizationStatus: authorizationStatus, 119 | create: create, 120 | destroy: destroy, 121 | dismissHeadingCalibrationDisplay: dismissHeadingCalibrationDisplay, 122 | heading: heading, 123 | headingAvailable: headingAvailable, 124 | isRangingAvailable: isRangingAvailable, 125 | location: location, 126 | locationServicesEnabled: locationServicesEnabled, 127 | maximumRegionMonitoringDistance: maximumRegionMonitoringDistance, 128 | monitoredRegions: monitoredRegions, 129 | requestAlwaysAuthorization: requestAlwaysAuthorization, 130 | requestLocation: requestLocation, 131 | requestWhenInUseAuthorization: requestWhenInUseAuthorization, 132 | set: set, 133 | significantLocationChangeMonitoringAvailable: significantLocationChangeMonitoringAvailable, 134 | startMonitoringForRegion: startMonitoringForRegion, 135 | startMonitoringSignificantLocationChanges: startMonitoringSignificantLocationChanges, 136 | startMonitoringVisits: startMonitoringVisits, 137 | startUpdatingHeading: startUpdatingHeading, 138 | startUpdatingLocation: startUpdatingLocation, 139 | stopMonitoringForRegion: stopMonitoringForRegion, 140 | stopMonitoringSignificantLocationChanges: stopMonitoringSignificantLocationChanges, 141 | stopMonitoringVisits: stopMonitoringVisits, 142 | stopUpdatingHeading: stopUpdatingHeading, 143 | stopUpdatingLocation: stopUpdatingLocation 144 | ) 145 | } 146 | } 147 | #endif 148 | 149 | public func _unimplemented( 150 | _ function: StaticString, file: StaticString = #file, line: UInt = #line 151 | ) -> Never { 152 | fatalError( 153 | """ 154 | `\(function)` was called but is not implemented. Be sure to provide an implementation for 155 | this endpoint when creating the mock. 156 | """, 157 | file: file, 158 | line: line 159 | ) 160 | } 161 | -------------------------------------------------------------------------------- /Sources/ComposableCoreLocation/Models/AccuracyAuthorization.swift: -------------------------------------------------------------------------------- 1 | // AccuracyAuthorization.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | import CoreLocation 5 | import Foundation 6 | 7 | /// A value type wrapper for `CLAccuracyAuthorization` 8 | public enum AccuracyAuthorization: Int { 9 | case fullAccuracy = 0 10 | case reducedAccuracy = 1 11 | } 12 | 13 | #if (compiler(>=5.3) && !(os(macOS) || targetEnvironment(macCatalyst))) || compiler(>=5.3.1) 14 | @available(iOS 14.0, macCatalyst 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 15 | extension AccuracyAuthorization { 16 | init?(_ accuracyAuth: CLAccuracyAuthorization?) { 17 | switch accuracyAuth { 18 | case .fullAccuracy: 19 | self = .fullAccuracy 20 | case .reducedAccuracy: 21 | self = .reducedAccuracy 22 | default: 23 | return nil 24 | } 25 | } 26 | } 27 | #endif 28 | -------------------------------------------------------------------------------- /Sources/ComposableCoreLocation/Models/Beacon.swift: -------------------------------------------------------------------------------- 1 | // Beacon.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | import CoreLocation 5 | 6 | /// A value type wrapper for `CLBeacon`. This type is necessary so that we can do equality checks 7 | /// and write tests against its values. 8 | @available(macOS, unavailable) 9 | @available(tvOS, unavailable) 10 | @available(watchOS, unavailable) 11 | public struct Beacon: Hashable { 12 | public var accuracy: CLLocationAccuracy 13 | public var major: NSNumber 14 | public var minor: NSNumber 15 | public var proximity: CLProximity 16 | public var rssi: Int 17 | public var timestamp: Date 18 | public var uuid: UUID 19 | 20 | public init(rawValue: CLBeacon) { 21 | accuracy = rawValue.accuracy 22 | major = rawValue.major 23 | minor = rawValue.minor 24 | proximity = rawValue.proximity 25 | rssi = rawValue.rssi 26 | timestamp = rawValue.timestamp 27 | uuid = rawValue.uuid 28 | } 29 | 30 | public init( 31 | accuracy: CLLocationAccuracy, 32 | major: NSNumber, 33 | minor: NSNumber, 34 | proximity: CLProximity, 35 | rssi: Int, 36 | timestamp: Date, 37 | uuid: UUID 38 | ) { 39 | self.accuracy = accuracy 40 | self.major = major 41 | self.minor = minor 42 | self.proximity = proximity 43 | self.rssi = rssi 44 | self.timestamp = timestamp 45 | self.uuid = uuid 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/ComposableCoreLocation/Models/Heading.swift: -------------------------------------------------------------------------------- 1 | // Heading.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | import CoreLocation 5 | 6 | /// A value type wrapper for `CLHeading`. This type is necessary so that we can do equality checks 7 | /// and write tests against its values. 8 | public struct Heading: Hashable { 9 | public var headingAccuracy: CLLocationDirection 10 | public var magneticHeading: CLLocationDirection 11 | public var timestamp: Date 12 | public var trueHeading: CLLocationDirection 13 | public var x: CLHeadingComponentValue 14 | public var y: CLHeadingComponentValue 15 | public var z: CLHeadingComponentValue 16 | 17 | @available(iOS 3, macCatalyst 13, watchOS 2, *) 18 | @available(macOS, unavailable) 19 | @available(tvOS, unavailable) 20 | public init(rawValue: CLHeading) { 21 | headingAccuracy = rawValue.headingAccuracy 22 | magneticHeading = rawValue.magneticHeading 23 | timestamp = rawValue.timestamp 24 | trueHeading = rawValue.trueHeading 25 | x = rawValue.x 26 | y = rawValue.y 27 | z = rawValue.z 28 | } 29 | 30 | public init( 31 | headingAccuracy: CLLocationDirection, 32 | magneticHeading: CLLocationDirection, 33 | timestamp: Date, 34 | trueHeading: CLLocationDirection, 35 | x: CLHeadingComponentValue, 36 | y: CLHeadingComponentValue, 37 | z: CLHeadingComponentValue 38 | ) { 39 | self.headingAccuracy = headingAccuracy 40 | self.magneticHeading = magneticHeading 41 | self.timestamp = timestamp 42 | self.trueHeading = trueHeading 43 | self.x = x 44 | self.y = y 45 | self.z = z 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/ComposableCoreLocation/Models/Location.swift: -------------------------------------------------------------------------------- 1 | // Location.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | import CoreLocation 5 | 6 | /// A value type wrapper for `CLLocation`. This type is necessary so that we can do equality checks 7 | /// and write tests against its values. 8 | @dynamicMemberLookup 9 | public struct Location { 10 | public let rawValue: CLLocation 11 | 12 | public init( 13 | altitude: CLLocationDistance = 0, 14 | coordinate: CLLocationCoordinate2D = CLLocationCoordinate2D(latitude: 0, longitude: 0), 15 | course: CLLocationDirection = 0, 16 | horizontalAccuracy: CLLocationAccuracy = 0, 17 | speed: CLLocationSpeed = 0, 18 | timestamp: Date = Date(), 19 | verticalAccuracy: CLLocationAccuracy = 0 20 | ) { 21 | rawValue = CLLocation( 22 | coordinate: coordinate, 23 | altitude: altitude, 24 | horizontalAccuracy: horizontalAccuracy, 25 | verticalAccuracy: verticalAccuracy, 26 | course: course, 27 | speed: speed, 28 | timestamp: timestamp 29 | ) 30 | } 31 | 32 | public init(rawValue: CLLocation) { 33 | self.rawValue = rawValue 34 | } 35 | 36 | public subscript(dynamicMember keyPath: KeyPath) -> T { 37 | rawValue[keyPath: keyPath] 38 | } 39 | } 40 | 41 | extension Location: Hashable { 42 | public static func == (lhs: Self, rhs: Self) -> Bool { 43 | let courseAccuracyIsEqual: Bool 44 | let speedAccuracyIsEqual: Bool 45 | #if compiler(>=5.2) 46 | if #available(iOS 13.4, macCatalyst 13.4, macOS 10.15.4, tvOS 13.4, watchOS 6.2, *) { 47 | courseAccuracyIsEqual = lhs.courseAccuracy == rhs.courseAccuracy 48 | speedAccuracyIsEqual = lhs.speedAccuracy == rhs.speedAccuracy 49 | } else { 50 | courseAccuracyIsEqual = true 51 | speedAccuracyIsEqual = true 52 | } 53 | #else 54 | courseAccuracyIsEqual = true 55 | speedAccuracyIsEqual = true 56 | #endif 57 | 58 | return lhs.altitude == rhs.altitude 59 | && lhs.coordinate.latitude == rhs.coordinate.latitude 60 | && lhs.coordinate.longitude == rhs.coordinate.longitude 61 | && lhs.course == rhs.course 62 | && lhs.floor == rhs.floor 63 | && lhs.horizontalAccuracy == rhs.horizontalAccuracy 64 | && lhs.speed == rhs.speed 65 | && lhs.timestamp == rhs.timestamp 66 | && lhs.verticalAccuracy == rhs.verticalAccuracy 67 | && speedAccuracyIsEqual 68 | && courseAccuracyIsEqual 69 | } 70 | 71 | public func hash(into hasher: inout Hasher) { 72 | hasher.combine(self.altitude) 73 | hasher.combine(self.coordinate.latitude) 74 | hasher.combine(self.coordinate.longitude) 75 | hasher.combine(self.course) 76 | hasher.combine(self.floor) 77 | hasher.combine(self.horizontalAccuracy) 78 | hasher.combine(self.speed) 79 | hasher.combine(self.timestamp) 80 | hasher.combine(self.verticalAccuracy) 81 | 82 | #if compiler(>=5.2) 83 | if #available(iOS 13.4, macCatalyst 13.4, macOS 10.15.4, tvOS 13.4, watchOS 6.2, *) { 84 | hasher.combine(self.speedAccuracy) 85 | hasher.combine(self.courseAccuracy) 86 | } 87 | #endif 88 | } 89 | } 90 | 91 | #if compiler(>=5.2) 92 | public extension Location { 93 | init( 94 | altitude: CLLocationDistance = 0, 95 | coordinate: CLLocationCoordinate2D = CLLocationCoordinate2D(latitude: 0, longitude: 0), 96 | course: CLLocationDirection = 0, 97 | courseAccuracy: Double = 0, 98 | horizontalAccuracy: CLLocationAccuracy = 0, 99 | speed: CLLocationSpeed = 0, 100 | speedAccuracy: Double = 0, 101 | timestamp: Date = Date(), 102 | verticalAccuracy: CLLocationAccuracy = 0 103 | ) { 104 | if #available(iOS 13.4, macCatalyst 13.4, macOS 10.15.4, tvOS 13.4, watchOS 6.2, *) { 105 | self.rawValue = CLLocation( 106 | coordinate: coordinate, 107 | altitude: altitude, 108 | horizontalAccuracy: horizontalAccuracy, 109 | verticalAccuracy: verticalAccuracy, 110 | course: course, 111 | courseAccuracy: courseAccuracy, 112 | speed: speed, 113 | speedAccuracy: speedAccuracy, 114 | timestamp: timestamp 115 | ) 116 | } else { 117 | rawValue = CLLocation( 118 | coordinate: coordinate, 119 | altitude: altitude, 120 | horizontalAccuracy: horizontalAccuracy, 121 | verticalAccuracy: verticalAccuracy, 122 | course: course, 123 | speed: speed, 124 | timestamp: timestamp 125 | ) 126 | } 127 | } 128 | } 129 | #endif 130 | 131 | extension Location: Codable { 132 | public init(from decoder: Decoder) throws { 133 | let values = try decoder.container(keyedBy: CodingKeys.self) 134 | let altitude = try values.decode(CLLocationDistance.self, forKey: .altitude) 135 | let latitude = try values.decode(CLLocationDegrees.self, forKey: .latitude) 136 | let longitude = try values.decode(CLLocationDegrees.self, forKey: .longitude) 137 | let course = try values.decode(CLLocationDirection.self, forKey: .course) 138 | let horizontalAccuracy = try values.decode(Double.self, forKey: .horizontalAccuracy) 139 | let speed = try values.decode(CLLocationSpeed.self, forKey: .speed) 140 | let timestamp = try values.decode(Date.self, forKey: .timestamp) 141 | let verticalAccuracy = try values.decode(CLLocationAccuracy.self, forKey: .verticalAccuracy) 142 | 143 | #if compiler(>=5.2) 144 | if #available(iOS 13.4, macCatalyst 13.4, macOS 10.15.4, tvOS 13.4, watchOS 6.2, *) { 145 | let courseAccuracy = try values.decode(Double.self, forKey: .courseAccuracy) 146 | let speedAccuracy = try values.decode(Double.self, forKey: .speedAccuracy) 147 | 148 | self.init( 149 | altitude: altitude, 150 | coordinate: CLLocationCoordinate2D(latitude: latitude, longitude: longitude), 151 | course: course, 152 | courseAccuracy: courseAccuracy, 153 | horizontalAccuracy: horizontalAccuracy, 154 | speed: speed, 155 | speedAccuracy: speedAccuracy, 156 | timestamp: timestamp, 157 | verticalAccuracy: verticalAccuracy 158 | ) 159 | } else { 160 | self.init( 161 | altitude: altitude, 162 | coordinate: CLLocationCoordinate2D(latitude: latitude, longitude: longitude), 163 | course: course, 164 | horizontalAccuracy: horizontalAccuracy, 165 | speed: speed, 166 | timestamp: timestamp, 167 | verticalAccuracy: verticalAccuracy 168 | ) 169 | } 170 | #else 171 | self.init( 172 | altitude: altitude, 173 | coordinate: CLLocationCoordinate2D(latitude: latitude, longitude: longitude), 174 | course: course, 175 | horizontalAccuracy: horizontalAccuracy, 176 | speed: speed, 177 | timestamp: timestamp, 178 | verticalAccuracy: verticalAccuracy 179 | ) 180 | #endif 181 | } 182 | 183 | public func encode(to encoder: Encoder) throws { 184 | var container = encoder.container(keyedBy: CodingKeys.self) 185 | try container.encode(rawValue.altitude, forKey: .altitude) 186 | try container.encode(rawValue.coordinate.latitude, forKey: .latitude) 187 | try container.encode(rawValue.coordinate.longitude, forKey: .longitude) 188 | try container.encode(rawValue.course, forKey: .course) 189 | try container.encode(rawValue.horizontalAccuracy, forKey: .horizontalAccuracy) 190 | try container.encode(rawValue.speed, forKey: .speed) 191 | try container.encode(rawValue.timestamp, forKey: .timestamp) 192 | try container.encode(rawValue.verticalAccuracy, forKey: .verticalAccuracy) 193 | 194 | #if compiler(>=5.2) 195 | if #available(iOS 13.4, macCatalyst 13.4, macOS 10.15.4, tvOS 13.4, watchOS 6.2, *) { 196 | try container.encode(rawValue.courseAccuracy, forKey: .courseAccuracy) 197 | } 198 | try container.encode(rawValue.speedAccuracy, forKey: .speedAccuracy) 199 | #endif 200 | } 201 | 202 | private enum CodingKeys: String, CodingKey { 203 | case latitude 204 | case longitude 205 | case altitude 206 | case course 207 | case courseAccuracy 208 | case horizontalAccuracy 209 | case speed 210 | case speedAccuracy 211 | case timestamp 212 | case verticalAccuracy 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /Sources/ComposableCoreLocation/Models/Region.swift: -------------------------------------------------------------------------------- 1 | // Region.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | import CoreLocation 5 | 6 | /// A value type wrapper for `CLRegion`. This type is necessary so that we can do equality checks 7 | /// and write tests against its values. 8 | public struct Region: Hashable { 9 | public let rawValue: CLRegion? 10 | 11 | public var identifier: String 12 | public var notifyOnEntry: Bool 13 | public var notifyOnExit: Bool 14 | 15 | init(rawValue: CLRegion) { 16 | self.rawValue = rawValue 17 | 18 | identifier = rawValue.identifier 19 | notifyOnEntry = rawValue.notifyOnEntry 20 | notifyOnExit = rawValue.notifyOnExit 21 | } 22 | 23 | init( 24 | identifier: String, 25 | notifyOnEntry: Bool, 26 | notifyOnExit: Bool 27 | ) { 28 | rawValue = nil 29 | 30 | self.identifier = identifier 31 | self.notifyOnEntry = notifyOnEntry 32 | self.notifyOnExit = notifyOnExit 33 | } 34 | 35 | public static func == (lhs: Self, rhs: Self) -> Bool { 36 | lhs.identifier == rhs.identifier 37 | && lhs.notifyOnEntry == rhs.notifyOnEntry 38 | && lhs.notifyOnExit == rhs.notifyOnExit 39 | } 40 | 41 | public func hash(into hasher: inout Hasher) { 42 | hasher.combine(identifier) 43 | hasher.combine(notifyOnExit) 44 | hasher.combine(notifyOnEntry) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/ComposableCoreLocation/Models/Visit.swift: -------------------------------------------------------------------------------- 1 | // Visit.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | import CoreLocation 5 | 6 | /// A value type wrapper for `CLVisit`. This type is necessary so that we can do equality checks 7 | /// and write tests against its values. 8 | @available(iOS 8, macCatalyst 13, *) 9 | @available(macOS, unavailable) 10 | @available(tvOS, unavailable) 11 | @available(watchOS, unavailable) 12 | public struct Visit: Hashable { 13 | public let rawValue: CLVisit? 14 | 15 | public var arrivalDate: Date 16 | public var coordinate: CLLocationCoordinate2D 17 | public var departureDate: Date 18 | public var horizontalAccuracy: CLLocationAccuracy 19 | 20 | init(visit: CLVisit) { 21 | rawValue = nil 22 | 23 | arrivalDate = visit.arrivalDate 24 | coordinate = visit.coordinate 25 | departureDate = visit.departureDate 26 | horizontalAccuracy = visit.horizontalAccuracy 27 | } 28 | 29 | public init( 30 | arrivalDate: Date, 31 | coordinate: CLLocationCoordinate2D, 32 | departureDate: Date, 33 | horizontalAccuracy: CLLocationAccuracy 34 | ) { 35 | rawValue = nil 36 | 37 | self.arrivalDate = arrivalDate 38 | self.coordinate = coordinate 39 | self.departureDate = departureDate 40 | self.horizontalAccuracy = horizontalAccuracy 41 | } 42 | 43 | public static func == (lhs: Self, rhs: Self) -> Bool { 44 | lhs.arrivalDate == rhs.arrivalDate 45 | && lhs.coordinate.latitude == rhs.coordinate.latitude 46 | && lhs.coordinate.longitude == rhs.coordinate.longitude 47 | && lhs.departureDate == rhs.departureDate 48 | && lhs.horizontalAccuracy == rhs.horizontalAccuracy 49 | } 50 | 51 | public func hash(into hasher: inout Hasher) { 52 | hasher.combine(arrivalDate) 53 | hasher.combine(coordinate.latitude) 54 | hasher.combine(coordinate.longitude) 55 | hasher.combine(departureDate) 56 | hasher.combine(horizontalAccuracy) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/ComposableCoreMotion/HeadphoneMotionManager/HeadphoneMotionManagerInterface.swift: -------------------------------------------------------------------------------- 1 | // HeadphoneMotionManagerInterface.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | #if canImport(CoreMotion) 5 | import ComposableArchitecture 6 | import CoreMotion 7 | 8 | /// A wrapper around Core Motion's `CMHeadphoneMotionManager` that exposes its functionality 9 | /// through effects and actions, making it easy to use with the Composable Architecture, and easy 10 | /// to test. 11 | @available(iOS 14, *) 12 | @available(macCatalyst 14, *) 13 | @available(macOS, unavailable) 14 | @available(tvOS, unavailable) 15 | @available(watchOS 7, *) 16 | public struct HeadphoneMotionManager { 17 | /// Actions that correspond to `CMHeadphoneMotionManagerDelegate` methods. 18 | /// 19 | /// See `CMHeadphoneMotionManagerDelegate` for more information. 20 | public enum Action: Equatable { 21 | case didConnect 22 | case didDisconnect 23 | } 24 | 25 | /// Creates a headphone motion manager. 26 | /// 27 | /// A motion manager must be first created before you can use its functionality, such as 28 | /// starting device motion updates or accessing data directly from the manager. 29 | public func create(id: AnyHashable) -> Effect { 30 | create(id) 31 | } 32 | 33 | /// Destroys a currently running headphone motion manager. 34 | /// 35 | /// In is good practice to destroy a headphone motion manager once you are done with it, such as 36 | /// when you leave a screen or no longer need motion data. 37 | public func destroy(id: AnyHashable) -> Effect { 38 | destroy(id) 39 | } 40 | 41 | /// The latest sample of device-motion data. 42 | public func deviceMotion(id: AnyHashable) -> DeviceMotion? { 43 | deviceMotion(id) 44 | } 45 | 46 | /// A Boolean value that determines whether the app is receiving updates from the device-motion 47 | /// service. 48 | public func isDeviceMotionActive(id: AnyHashable) -> Bool { 49 | isDeviceMotionActive(id) 50 | } 51 | 52 | /// A Boolean value that indicates whether the device-motion service is available on the device. 53 | public func isDeviceMotionAvailable(id: AnyHashable) -> Bool { 54 | isDeviceMotionAvailable(id) 55 | } 56 | 57 | /// Starts device-motion updates without a block handler. 58 | /// 59 | /// Returns a long-living effect that emits device motion data each time the headphone motion 60 | /// manager receives a new value. 61 | public func startDeviceMotionUpdates( 62 | id: AnyHashable, 63 | to queue: OperationQueue = .main 64 | ) -> Effect { 65 | startDeviceMotionUpdates(id, queue) 66 | } 67 | 68 | /// Stops device-motion updates. 69 | public func stopDeviceMotionUpdates(id: AnyHashable) -> Effect { 70 | stopDeviceMotionUpdates(id) 71 | } 72 | 73 | public init( 74 | create: @escaping (AnyHashable) -> Effect, 75 | destroy: @escaping (AnyHashable) -> Effect, 76 | deviceMotion: @escaping (AnyHashable) -> DeviceMotion?, 77 | isDeviceMotionActive: @escaping (AnyHashable) -> Bool, 78 | isDeviceMotionAvailable: @escaping (AnyHashable) -> Bool, 79 | startDeviceMotionUpdates: @escaping (AnyHashable, OperationQueue) -> 80 | Effect, 81 | stopDeviceMotionUpdates: @escaping (AnyHashable) -> Effect 82 | ) { 83 | self.create = create 84 | self.destroy = destroy 85 | self.deviceMotion = deviceMotion 86 | self.isDeviceMotionActive = isDeviceMotionActive 87 | self.isDeviceMotionAvailable = isDeviceMotionAvailable 88 | self.startDeviceMotionUpdates = startDeviceMotionUpdates 89 | self.stopDeviceMotionUpdates = stopDeviceMotionUpdates 90 | } 91 | 92 | var create: (AnyHashable) -> Effect 93 | var destroy: (AnyHashable) -> Effect 94 | var deviceMotion: (AnyHashable) -> DeviceMotion? 95 | var isDeviceMotionActive: (AnyHashable) -> Bool 96 | var isDeviceMotionAvailable: (AnyHashable) -> Bool 97 | var startDeviceMotionUpdates: (AnyHashable, OperationQueue) -> Effect 98 | var stopDeviceMotionUpdates: (AnyHashable) -> Effect 99 | } 100 | #endif 101 | -------------------------------------------------------------------------------- /Sources/ComposableCoreMotion/HeadphoneMotionManager/HeadphoneMotionManagerLive.swift: -------------------------------------------------------------------------------- 1 | // HeadphoneMotionManagerLive.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | #if canImport(CoreMotion) && compiler(>=5.3) && (os(iOS) || os(watchOS) || targetEnvironment(macCatalyst)) 5 | import Combine 6 | import ComposableArchitecture 7 | import CoreMotion 8 | 9 | @available(iOS 14, *) 10 | @available(macCatalyst 14, *) 11 | @available(macOS, unavailable) 12 | @available(tvOS, unavailable) 13 | @available(watchOS 7, *) 14 | extension HeadphoneMotionManager { 15 | public static let live = HeadphoneMotionManager( 16 | create: { id in 17 | Effect.run { subscriber in 18 | if dependencies[id] != nil { 19 | assertionFailure( 20 | """ 21 | You are attempting to create a headphone motion manager with the id \(id), but there \ 22 | is already a running manager with that id. This is considered a programmer error \ 23 | since you may be accidentally overwriting an existing manager without knowing. 24 | 25 | To fix you should either destroy the existing manager before creating a new one, or \ 26 | you should not try creating a new one before this one is destroyed. 27 | """) 28 | } 29 | 30 | let manager = CMHeadphoneMotionManager() 31 | var delegate = Delegate(subscriber) 32 | manager.delegate = delegate 33 | 34 | dependencies[id] = Dependencies( 35 | delegate: delegate, 36 | manager: manager, 37 | subscriber: subscriber 38 | ) 39 | 40 | return AnyCancellable { 41 | dependencies[id] = nil 42 | } 43 | } 44 | }, 45 | destroy: { id in 46 | .fireAndForget { dependencies[id] = nil } 47 | }, 48 | deviceMotion: { id in 49 | requireHeadphoneMotionManager(id: id)?.deviceMotion.map(DeviceMotion.init) 50 | }, 51 | isDeviceMotionActive: { id in 52 | requireHeadphoneMotionManager(id: id)?.isDeviceMotionActive ?? false 53 | }, 54 | isDeviceMotionAvailable: { id in 55 | requireHeadphoneMotionManager(id: id)?.isDeviceMotionAvailable ?? false 56 | }, 57 | startDeviceMotionUpdates: { id, queue in 58 | Effect.run { subscriber in 59 | guard let manager = requireHeadphoneMotionManager(id: id) 60 | else { 61 | couldNotFindHeadphoneMotionManager(id: id) 62 | return AnyCancellable {} 63 | } 64 | guard deviceMotionUpdatesSubscribers[id] == nil 65 | else { return AnyCancellable {} } 66 | 67 | deviceMotionUpdatesSubscribers[id] = subscriber 68 | manager.startDeviceMotionUpdates(to: queue) { data, error in 69 | if let data = data { 70 | subscriber.send(.init(data)) 71 | } else if let error = error { 72 | subscriber.send(completion: .failure(error)) 73 | } 74 | } 75 | return AnyCancellable { 76 | manager.stopDeviceMotionUpdates() 77 | } 78 | } 79 | }, 80 | stopDeviceMotionUpdates: { id in 81 | .fireAndForget { 82 | guard let manager = requireHeadphoneMotionManager(id: id) 83 | else { 84 | couldNotFindHeadphoneMotionManager(id: id) 85 | return 86 | } 87 | manager.stopDeviceMotionUpdates() 88 | deviceMotionUpdatesSubscribers[id]?.send(completion: .finished) 89 | deviceMotionUpdatesSubscribers[id] = nil 90 | } 91 | } 92 | ) 93 | 94 | private class Delegate: NSObject, CMHeadphoneMotionManagerDelegate { 95 | let subscriber: Effect.Subscriber 96 | 97 | init(_ subscriber: Effect.Subscriber) { 98 | self.subscriber = subscriber 99 | } 100 | 101 | func headphoneMotionManagerDidConnect(_: CMHeadphoneMotionManager) { 102 | subscriber.send(.didConnect) 103 | } 104 | 105 | func headphoneMotionManagerDidDisconnect(_: CMHeadphoneMotionManager) { 106 | subscriber.send(.didDisconnect) 107 | } 108 | } 109 | 110 | private struct Dependencies { 111 | let delegate: Delegate 112 | let manager: CMHeadphoneMotionManager 113 | let subscriber: Effect.Subscriber 114 | } 115 | 116 | private static var dependencies: [AnyHashable: Dependencies] = [:] 117 | 118 | private static func requireHeadphoneMotionManager(id: AnyHashable) 119 | -> CMHeadphoneMotionManager? 120 | { 121 | if dependencies[id] == nil { 122 | couldNotFindHeadphoneMotionManager(id: id) 123 | } 124 | return dependencies[id]?.manager 125 | } 126 | } 127 | 128 | private var deviceMotionUpdatesSubscribers: 129 | [AnyHashable: Effect.Subscriber] = [:] 130 | 131 | private func couldNotFindHeadphoneMotionManager(id: Any) { 132 | assertionFailure( 133 | """ 134 | A headphone motion manager could not be found with the id \(id). This is considered a \ 135 | programmer error. You should not invoke methods on a motion manager before it has been \ 136 | created or after it has been destroyed. Refactor your code to make sure there is a headphone \ 137 | motion manager created by the time you invoke this endpoint. 138 | """) 139 | } 140 | #endif 141 | -------------------------------------------------------------------------------- /Sources/ComposableCoreMotion/HeadphoneMotionManager/HeadphoneMotionManagerMock.swift: -------------------------------------------------------------------------------- 1 | // HeadphoneMotionManagerMock.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | #if canImport(CoreMotion) 5 | import ComposableArchitecture 6 | 7 | @available(iOS 14, *) 8 | @available(macCatalyst 14, *) 9 | @available(macOS, unavailable) 10 | @available(tvOS, unavailable) 11 | @available(watchOS 7, *) 12 | public extension HeadphoneMotionManager { 13 | static func unimplemented( 14 | create: @escaping (AnyHashable) -> Effect = { _ in _unimplemented("create") }, 15 | destroy: @escaping (AnyHashable) -> Effect = { _ in _unimplemented("destroy") }, 16 | deviceMotion: @escaping (AnyHashable) -> DeviceMotion? = { _ in _unimplemented("deviceMotion") 17 | }, 18 | isDeviceMotionActive: @escaping (AnyHashable) -> Bool = { _ in 19 | _unimplemented("isDeviceMotionActive") 20 | }, 21 | isDeviceMotionAvailable: @escaping (AnyHashable) -> Bool = { _ in 22 | _unimplemented("isDeviceMotionAvailable") 23 | }, 24 | startDeviceMotionUpdates: @escaping (AnyHashable, OperationQueue) -> 25 | Effect = { _, _ in _unimplemented("startDeviceMotionUpdates") }, 26 | stopDeviceMotionUpdates: @escaping (AnyHashable) -> Effect = { _ in 27 | _unimplemented("stopDeviceMotionUpdates") 28 | } 29 | ) -> HeadphoneMotionManager { 30 | Self( 31 | create: create, 32 | destroy: destroy, 33 | deviceMotion: deviceMotion, 34 | isDeviceMotionActive: isDeviceMotionActive, 35 | isDeviceMotionAvailable: isDeviceMotionAvailable, 36 | startDeviceMotionUpdates: startDeviceMotionUpdates, 37 | stopDeviceMotionUpdates: stopDeviceMotionUpdates 38 | ) 39 | } 40 | } 41 | #endif 42 | -------------------------------------------------------------------------------- /Sources/ComposableCoreMotion/Internal/Debug.swift: -------------------------------------------------------------------------------- 1 | // Debug.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | #if canImport(CoreLocation) 5 | import ComposableArchitecture 6 | import CoreLocation 7 | 8 | extension CLAuthorizationStatus: CustomDebugOutputConvertible { 9 | public var debugOutput: String { 10 | switch self { 11 | case .notDetermined: 12 | return "notDetermined" 13 | case .restricted: 14 | return "restricted" 15 | case .denied: 16 | return "denied" 17 | case .authorizedAlways: 18 | return "authorizedAlways" 19 | case .authorizedWhenInUse: 20 | return "authorizedWhenInUse" 21 | @unknown default: 22 | return "unknown" 23 | } 24 | } 25 | } 26 | #endif 27 | -------------------------------------------------------------------------------- /Sources/ComposableCoreMotion/Internal/Exports.swift: -------------------------------------------------------------------------------- 1 | // Exports.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | #if canImport(CoreMotion) 5 | @_exported import ComposableArchitecture 6 | @_exported import CoreMotion 7 | #endif 8 | -------------------------------------------------------------------------------- /Sources/ComposableCoreMotion/Internal/Unimplemented.swift: -------------------------------------------------------------------------------- 1 | // Unimplemented.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | public func _unimplemented( 5 | _ function: StaticString, file: StaticString = #file, line: UInt = #line 6 | ) -> Never { 7 | fatalError( 8 | """ 9 | `\(function)` was called but is not implemented. Be sure to provide an implementation for 10 | this endpoint when creating the mock. 11 | """, 12 | file: file, 13 | line: line 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /Sources/ComposableCoreMotion/Live.swift: -------------------------------------------------------------------------------- 1 | // Live.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | #if canImport(CoreMotion) 5 | import Combine 6 | import ComposableArchitecture 7 | import CoreMotion 8 | 9 | @available(iOS 4, *) 10 | @available(macCatalyst 13, *) 11 | @available(macOS, unavailable) 12 | @available(tvOS, unavailable) 13 | @available(watchOS 2, *) 14 | extension MotionManager { 15 | public static let live = MotionManager( 16 | accelerometerData: { id in 17 | requireMotionManager(id: id)?.accelerometerData.map(AccelerometerData.init) 18 | }, 19 | attitudeReferenceFrame: { id in 20 | requireMotionManager(id: id)?.attitudeReferenceFrame ?? .init() 21 | }, 22 | availableAttitudeReferenceFrames: { 23 | CMMotionManager.availableAttitudeReferenceFrames() 24 | }, 25 | create: { id in 26 | .fireAndForget { 27 | if managers[id] != nil { 28 | assertionFailure( 29 | """ 30 | You are attempting to create a motion manager with the id \(id), but there is already \ 31 | a running manager with that id. This is considered a programmer error since you may \ 32 | be accidentally overwriting an existing manager without knowing. 33 | 34 | To fix you should either destroy the existing manager before creating a new one, or \ 35 | you should not try creating a new one before this one is destroyed. 36 | """) 37 | } 38 | managers[id] = CMMotionManager() 39 | } 40 | }, 41 | destroy: { id in 42 | .fireAndForget { managers[id] = nil } 43 | }, 44 | deviceMotion: { id in 45 | requireMotionManager(id: id)?.deviceMotion.map(DeviceMotion.init) 46 | }, 47 | gyroData: { id in 48 | requireMotionManager(id: id)?.gyroData.map(GyroData.init) 49 | }, 50 | isAccelerometerActive: { id in 51 | requireMotionManager(id: id)?.isAccelerometerActive ?? false 52 | }, 53 | isAccelerometerAvailable: { id in 54 | requireMotionManager(id: id)?.isAccelerometerAvailable ?? false 55 | }, 56 | isDeviceMotionActive: { id in 57 | requireMotionManager(id: id)?.isDeviceMotionActive ?? false 58 | }, 59 | isDeviceMotionAvailable: { id in 60 | requireMotionManager(id: id)?.isDeviceMotionAvailable ?? false 61 | }, 62 | isGyroActive: { id in 63 | requireMotionManager(id: id)?.isGyroActive ?? false 64 | }, 65 | isGyroAvailable: { id in 66 | requireMotionManager(id: id)?.isGyroAvailable ?? false 67 | }, 68 | isMagnetometerActive: { id in 69 | requireMotionManager(id: id)?.isDeviceMotionActive ?? false 70 | }, 71 | isMagnetometerAvailable: { id in 72 | requireMotionManager(id: id)?.isMagnetometerAvailable ?? false 73 | }, 74 | magnetometerData: { id in 75 | requireMotionManager(id: id)?.magnetometerData.map(MagnetometerData.init) 76 | }, 77 | set: { id, properties in 78 | .fireAndForget { 79 | guard let manager = requireMotionManager(id: id) 80 | else { 81 | return 82 | } 83 | 84 | if let accelerometerUpdateInterval = properties.accelerometerUpdateInterval { 85 | manager.accelerometerUpdateInterval = accelerometerUpdateInterval 86 | } 87 | if let deviceMotionUpdateInterval = properties.deviceMotionUpdateInterval { 88 | manager.deviceMotionUpdateInterval = deviceMotionUpdateInterval 89 | } 90 | if let gyroUpdateInterval = properties.gyroUpdateInterval { 91 | manager.gyroUpdateInterval = gyroUpdateInterval 92 | } 93 | if let magnetometerUpdateInterval = properties.magnetometerUpdateInterval { 94 | manager.magnetometerUpdateInterval = magnetometerUpdateInterval 95 | } 96 | if let showsDeviceMovementDisplay = properties.showsDeviceMovementDisplay { 97 | manager.showsDeviceMovementDisplay = showsDeviceMovementDisplay 98 | } 99 | } 100 | }, 101 | startAccelerometerUpdates: { id, queue in 102 | Effect.run { subscriber in 103 | guard let manager = requireMotionManager(id: id) 104 | else { 105 | return AnyCancellable {} 106 | } 107 | guard accelerometerUpdatesSubscribers[id] == nil 108 | else { return AnyCancellable {} } 109 | 110 | accelerometerUpdatesSubscribers[id] = subscriber 111 | manager.startAccelerometerUpdates(to: queue) { data, error in 112 | if let data = data { 113 | subscriber.send(.init(data)) 114 | } else if let error = error { 115 | subscriber.send(completion: .failure(error)) 116 | } 117 | } 118 | return AnyCancellable { 119 | manager.stopAccelerometerUpdates() 120 | } 121 | } 122 | }, 123 | startDeviceMotionUpdates: { id, frame, queue in 124 | Effect.run { subscriber in 125 | guard let manager = requireMotionManager(id: id) 126 | else { 127 | return AnyCancellable {} 128 | } 129 | guard deviceMotionUpdatesSubscribers[id] == nil 130 | else { return AnyCancellable {} } 131 | 132 | deviceMotionUpdatesSubscribers[id] = subscriber 133 | manager.startDeviceMotionUpdates(using: frame, to: queue) { data, error in 134 | if let data = data { 135 | subscriber.send(.init(data)) 136 | } else if let error = error { 137 | subscriber.send(completion: .failure(error)) 138 | } 139 | } 140 | return AnyCancellable { 141 | manager.stopDeviceMotionUpdates() 142 | } 143 | } 144 | }, 145 | startGyroUpdates: { id, queue in 146 | Effect.run { subscriber in 147 | guard let manager = requireMotionManager(id: id) 148 | else { 149 | return AnyCancellable {} 150 | } 151 | guard deviceGyroUpdatesSubscribers[id] == nil 152 | else { return AnyCancellable {} } 153 | 154 | deviceGyroUpdatesSubscribers[id] = subscriber 155 | manager.startGyroUpdates(to: queue) { data, error in 156 | if let data = data { 157 | subscriber.send(.init(data)) 158 | } else if let error = error { 159 | subscriber.send(completion: .failure(error)) 160 | } 161 | } 162 | return AnyCancellable { 163 | manager.stopGyroUpdates() 164 | } 165 | } 166 | }, 167 | startMagnetometerUpdates: { id, queue in 168 | Effect.run { subscriber in 169 | guard let manager = managers[id] 170 | else { 171 | couldNotFindMotionManager(id: id) 172 | return AnyCancellable {} 173 | } 174 | guard deviceMagnetometerUpdatesSubscribers[id] == nil 175 | else { return AnyCancellable {} } 176 | 177 | deviceMagnetometerUpdatesSubscribers[id] = subscriber 178 | manager.startMagnetometerUpdates(to: queue) { data, error in 179 | if let data = data { 180 | subscriber.send(.init(data)) 181 | } else if let error = error { 182 | subscriber.send(completion: .failure(error)) 183 | } 184 | } 185 | return AnyCancellable { 186 | manager.stopMagnetometerUpdates() 187 | } 188 | } 189 | }, 190 | stopAccelerometerUpdates: { id in 191 | .fireAndForget { 192 | guard let manager = managers[id] 193 | else { 194 | couldNotFindMotionManager(id: id) 195 | return 196 | } 197 | manager.stopAccelerometerUpdates() 198 | accelerometerUpdatesSubscribers[id]?.send(completion: .finished) 199 | accelerometerUpdatesSubscribers[id] = nil 200 | } 201 | }, 202 | stopDeviceMotionUpdates: { id in 203 | .fireAndForget { 204 | guard let manager = managers[id] 205 | else { 206 | couldNotFindMotionManager(id: id) 207 | return 208 | } 209 | manager.stopDeviceMotionUpdates() 210 | deviceMotionUpdatesSubscribers[id]?.send(completion: .finished) 211 | deviceMotionUpdatesSubscribers[id] = nil 212 | } 213 | }, 214 | stopGyroUpdates: { id in 215 | .fireAndForget { 216 | guard let manager = managers[id] 217 | else { 218 | couldNotFindMotionManager(id: id) 219 | return 220 | } 221 | manager.stopGyroUpdates() 222 | deviceGyroUpdatesSubscribers[id]?.send(completion: .finished) 223 | deviceGyroUpdatesSubscribers[id] = nil 224 | } 225 | }, 226 | stopMagnetometerUpdates: { id in 227 | .fireAndForget { 228 | guard let manager = managers[id] 229 | else { 230 | couldNotFindMotionManager(id: id) 231 | return 232 | } 233 | manager.stopMagnetometerUpdates() 234 | deviceMagnetometerUpdatesSubscribers[id]?.send(completion: .finished) 235 | deviceMagnetometerUpdatesSubscribers[id] = nil 236 | } 237 | } 238 | ) 239 | 240 | private static var managers: [AnyHashable: CMMotionManager] = [:] 241 | 242 | private static func requireMotionManager(id: AnyHashable) -> CMMotionManager? { 243 | if managers[id] == nil { 244 | couldNotFindMotionManager(id: id) 245 | } 246 | return managers[id] 247 | } 248 | } 249 | 250 | private var accelerometerUpdatesSubscribers: 251 | [AnyHashable: Effect.Subscriber] = [:] 252 | private var deviceMotionUpdatesSubscribers: 253 | [AnyHashable: Effect.Subscriber] = 254 | [:] 255 | private var deviceGyroUpdatesSubscribers: [AnyHashable: Effect.Subscriber] = [:] 256 | private var deviceMagnetometerUpdatesSubscribers: 257 | [AnyHashable: Effect.Subscriber] = [:] 258 | 259 | private func couldNotFindMotionManager(id: Any) { 260 | assertionFailure( 261 | """ 262 | A motion manager could not be found with the id \(id). This is considered a programmer error. \ 263 | You should not invoke methods on a motion manager before it has been created or after it \ 264 | has been destroyed. Refactor your code to make sure there is a motion manager created by the \ 265 | time you invoke this endpoint. 266 | """) 267 | } 268 | #endif 269 | -------------------------------------------------------------------------------- /Sources/ComposableCoreMotion/Mock.swift: -------------------------------------------------------------------------------- 1 | // Mock.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | #if canImport(CoreMotion) 5 | import ComposableArchitecture 6 | 7 | @available(iOS 4.0, *) 8 | @available(macCatalyst 13.0, *) 9 | @available(macOS, unavailable) 10 | @available(tvOS, unavailable) 11 | @available(watchOS 2.0, *) 12 | public extension MotionManager { 13 | static func unimplemented( 14 | accelerometerData: @escaping (AnyHashable) -> AccelerometerData? = { _ in 15 | _unimplemented("accelerometerData") 16 | }, 17 | attitudeReferenceFrame: @escaping (AnyHashable) -> CMAttitudeReferenceFrame = { _ in 18 | _unimplemented("attitudeReferenceFrame") 19 | }, 20 | availableAttitudeReferenceFrames: @escaping () -> CMAttitudeReferenceFrame = { 21 | _unimplemented("availableAttitudeReferenceFrames") 22 | }, 23 | create: @escaping (AnyHashable) -> Effect = { _ in _unimplemented("create") }, 24 | destroy: @escaping (AnyHashable) -> Effect = { _ in _unimplemented("destroy") }, 25 | deviceMotion: @escaping (AnyHashable) -> DeviceMotion? = { _ in _unimplemented("deviceMotion") 26 | }, 27 | gyroData: @escaping (AnyHashable) -> GyroData? = { _ in _unimplemented("gyroData") }, 28 | isAccelerometerActive: @escaping (AnyHashable) -> Bool = { _ in 29 | _unimplemented("isAccelerometerActive") 30 | }, 31 | isAccelerometerAvailable: @escaping (AnyHashable) -> Bool = { _ in 32 | _unimplemented("isAccelerometerAvailable") 33 | }, 34 | isDeviceMotionActive: @escaping (AnyHashable) -> Bool = { _ in 35 | _unimplemented("isDeviceMotionActive") 36 | }, 37 | isDeviceMotionAvailable: @escaping (AnyHashable) -> Bool = { _ in 38 | _unimplemented("isDeviceMotionAvailable") 39 | }, 40 | isGyroActive: @escaping (AnyHashable) -> Bool = { _ in _unimplemented("isGyroActive") }, 41 | isGyroAvailable: @escaping (AnyHashable) -> Bool = { _ in _unimplemented("isGyroAvailable") }, 42 | isMagnetometerActive: @escaping (AnyHashable) -> Bool = { _ in 43 | _unimplemented("isMagnetometerActive") 44 | }, 45 | isMagnetometerAvailable: @escaping (AnyHashable) -> Bool = { _ in 46 | _unimplemented("isMagnetometerAvailable") 47 | }, 48 | magnetometerData: @escaping (AnyHashable) -> MagnetometerData? = { _ in 49 | _unimplemented("magnetometerData") 50 | }, 51 | set: @escaping (AnyHashable, MotionManager.Properties) -> Effect = { _, _ in 52 | _unimplemented("set") 53 | }, 54 | startAccelerometerUpdates: @escaping (AnyHashable, OperationQueue) -> Effect< 55 | AccelerometerData, Error 56 | > = { _, _ in _unimplemented("startAccelerometerUpdates") }, 57 | startDeviceMotionUpdates: @escaping (AnyHashable, CMAttitudeReferenceFrame, OperationQueue) -> 58 | Effect = { _, _, _ in _unimplemented("startDeviceMotionUpdates") }, 59 | startGyroUpdates: @escaping (AnyHashable, OperationQueue) -> Effect = { 60 | _, _ in 61 | _unimplemented("startGyroUpdates") 62 | }, 63 | startMagnetometerUpdates: @escaping (AnyHashable, OperationQueue) -> Effect< 64 | MagnetometerData, Error 65 | > = { _, _ in _unimplemented("startMagnetometerUpdates") }, 66 | stopAccelerometerUpdates: @escaping (AnyHashable) -> Effect = { _ in 67 | _unimplemented("stopAccelerometerUpdates") 68 | }, 69 | stopDeviceMotionUpdates: @escaping (AnyHashable) -> Effect = { _ in 70 | _unimplemented("stopDeviceMotionUpdates") 71 | }, 72 | stopGyroUpdates: @escaping (AnyHashable) -> Effect = { _ in 73 | _unimplemented("stopGyroUpdates") 74 | }, 75 | stopMagnetometerUpdates: @escaping (AnyHashable) -> Effect = { _ in 76 | _unimplemented("stopMagnetometerUpdates") 77 | } 78 | ) -> MotionManager { 79 | Self( 80 | accelerometerData: accelerometerData, 81 | attitudeReferenceFrame: attitudeReferenceFrame, 82 | availableAttitudeReferenceFrames: availableAttitudeReferenceFrames, 83 | create: create, 84 | destroy: destroy, 85 | deviceMotion: deviceMotion, 86 | gyroData: gyroData, 87 | isAccelerometerActive: isAccelerometerActive, 88 | isAccelerometerAvailable: isAccelerometerAvailable, 89 | isDeviceMotionActive: isDeviceMotionActive, 90 | isDeviceMotionAvailable: isDeviceMotionAvailable, 91 | isGyroActive: isGyroActive, 92 | isGyroAvailable: isGyroAvailable, 93 | isMagnetometerActive: isMagnetometerActive, 94 | isMagnetometerAvailable: isMagnetometerAvailable, 95 | magnetometerData: magnetometerData, 96 | set: set, 97 | startAccelerometerUpdates: startAccelerometerUpdates, 98 | startDeviceMotionUpdates: startDeviceMotionUpdates, 99 | startGyroUpdates: startGyroUpdates, 100 | startMagnetometerUpdates: startMagnetometerUpdates, 101 | stopAccelerometerUpdates: stopAccelerometerUpdates, 102 | stopDeviceMotionUpdates: stopDeviceMotionUpdates, 103 | stopGyroUpdates: stopGyroUpdates, 104 | stopMagnetometerUpdates: stopMagnetometerUpdates 105 | ) 106 | } 107 | } 108 | #endif 109 | -------------------------------------------------------------------------------- /Sources/ComposableCoreMotion/Models/AccelerometerData.swift: -------------------------------------------------------------------------------- 1 | // AccelerometerData.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | #if canImport(CoreMotion) 5 | import CoreMotion 6 | 7 | /// A data sample from the device's three accelerometers. 8 | /// 9 | /// See the documentation for `CMAccelerometerData` for more info. 10 | public struct AccelerometerData: Hashable { 11 | public var acceleration: CMAcceleration 12 | 13 | public init(_ accelerometerData: CMAccelerometerData) { 14 | acceleration = accelerometerData.acceleration 15 | } 16 | 17 | public init(acceleration: CMAcceleration) { 18 | self.acceleration = acceleration 19 | } 20 | 21 | public static func == (lhs: Self, rhs: Self) -> Bool { 22 | lhs.acceleration.x == rhs.acceleration.x 23 | && lhs.acceleration.y == rhs.acceleration.y 24 | && lhs.acceleration.z == rhs.acceleration.z 25 | } 26 | 27 | public func hash(into hasher: inout Hasher) { 28 | hasher.combine(acceleration.x) 29 | hasher.combine(acceleration.y) 30 | hasher.combine(acceleration.z) 31 | } 32 | } 33 | #endif 34 | -------------------------------------------------------------------------------- /Sources/ComposableCoreMotion/Models/Attitude.swift: -------------------------------------------------------------------------------- 1 | // Attitude.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | #if canImport(CoreMotion) 5 | import CoreMotion 6 | 7 | /// The device's orientation relative to a known frame of reference at a point in time. 8 | /// 9 | /// See the documentation for `CMAttitude` for more info. 10 | public struct Attitude: Hashable { 11 | public var quaternion: CMQuaternion 12 | 13 | public init(_ attitude: CMAttitude) { 14 | quaternion = attitude.quaternion 15 | } 16 | 17 | public init(quaternion: CMQuaternion) { 18 | self.quaternion = quaternion 19 | } 20 | 21 | @inlinable 22 | public func multiply(byInverseOf attitude: Self) -> Self { 23 | .init(quaternion: quaternion.multiplied(by: attitude.quaternion.inverse)) 24 | } 25 | 26 | @inlinable 27 | public var rotationMatrix: CMRotationMatrix { 28 | let q = quaternion 29 | 30 | let s = 31 | 1 32 | / (quaternion.w * quaternion.w 33 | + quaternion.x * quaternion.x 34 | + quaternion.y * quaternion.y 35 | + quaternion.z * quaternion.z) 36 | 37 | var matrix = CMRotationMatrix() 38 | 39 | matrix.m11 = 1 - 2 * s * (q.y * q.y + q.z * q.z) 40 | matrix.m12 = 2 * s * (q.x * q.y - q.z * q.w) 41 | matrix.m13 = 2 * s * (q.x * q.z + q.y * q.w) 42 | 43 | matrix.m21 = 2 * s * (q.x * q.y + q.z * q.w) 44 | matrix.m22 = 1 - 2 * s * (q.x * q.x + q.z * q.z) 45 | matrix.m23 = 2 * s * (q.y * q.z - q.x * q.w) 46 | 47 | matrix.m31 = 2 * s * (q.x * q.z - q.y * q.w) 48 | matrix.m32 = 2 * s * (q.y * q.z + q.x * q.w) 49 | matrix.m33 = 1 - 2 * s * (q.x * q.x + q.y * q.y) 50 | 51 | return matrix 52 | } 53 | 54 | @inlinable 55 | public var roll: Double { 56 | let q = quaternion 57 | return atan2( 58 | 2 * (q.w * q.x + q.y * q.z), 59 | 1 - 2 * (q.x * q.x + q.y * q.y) 60 | ) 61 | } 62 | 63 | @inlinable 64 | public var pitch: Double { 65 | let q = quaternion 66 | let p = 2 * (q.w * q.y - q.z * q.x) 67 | return p > 1 68 | ? Double.pi / 2 69 | : p < -1 70 | ? -Double.pi / 2 71 | : asin(p) 72 | } 73 | 74 | @inlinable 75 | public var yaw: Double { 76 | let q = quaternion 77 | return atan2( 78 | 2 * (q.w * q.z + q.x * q.y), 79 | 1 - 2 * (q.y * q.y + q.z * q.z) 80 | ) 81 | } 82 | 83 | public static func == (lhs: Self, rhs: Self) -> Bool { 84 | lhs.quaternion.w == rhs.quaternion.w 85 | && lhs.quaternion.x == rhs.quaternion.x 86 | && lhs.quaternion.y == rhs.quaternion.y 87 | && lhs.quaternion.z == rhs.quaternion.z 88 | } 89 | 90 | public func hash(into hasher: inout Hasher) { 91 | hasher.combine(quaternion.w) 92 | hasher.combine(quaternion.x) 93 | hasher.combine(quaternion.y) 94 | hasher.combine(quaternion.z) 95 | } 96 | } 97 | 98 | extension CMQuaternion { 99 | @usableFromInline 100 | var inverse: CMQuaternion { 101 | let invSumOfSquares = 102 | 1 / (x * x + y * y + z * z + w * w) 103 | return CMQuaternion( 104 | x: -x * invSumOfSquares, 105 | y: -y * invSumOfSquares, 106 | z: -z * invSumOfSquares, 107 | w: w * invSumOfSquares 108 | ) 109 | } 110 | 111 | @usableFromInline 112 | func multiplied(by other: Self) -> Self { 113 | var result = self 114 | result.w = w * other.w - x * other.x - y * other.y - z * other.z 115 | result.x = w * other.x + x * other.w + y * other.z - z * other.y 116 | result.y = w * other.y - x * other.z + y * other.w + z * other.x 117 | result.z = w * other.z + x * other.y - y * other.x + z * other.w 118 | return result 119 | } 120 | } 121 | #endif 122 | -------------------------------------------------------------------------------- /Sources/ComposableCoreMotion/Models/DeviceMotion.swift: -------------------------------------------------------------------------------- 1 | // DeviceMotion.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | #if canImport(CoreMotion) 5 | import CoreMotion 6 | 7 | /// Encapsulated measurements of the attitude, rotation rate, and acceleration of a device. 8 | /// 9 | /// See the documentation for `CMDeviceMotion` for more info. 10 | public struct DeviceMotion: Hashable { 11 | public var attitude: Attitude 12 | public var gravity: CMAcceleration 13 | public var heading: Double 14 | public var magneticField: CMCalibratedMagneticField 15 | public var rotationRate: CMRotationRate 16 | public var timestamp: TimeInterval 17 | public var userAcceleration: CMAcceleration 18 | 19 | public init(_ deviceMotion: CMDeviceMotion) { 20 | attitude = Attitude(deviceMotion.attitude) 21 | gravity = deviceMotion.gravity 22 | heading = deviceMotion.heading 23 | magneticField = deviceMotion.magneticField 24 | rotationRate = deviceMotion.rotationRate 25 | timestamp = deviceMotion.timestamp 26 | userAcceleration = deviceMotion.userAcceleration 27 | } 28 | 29 | public init( 30 | attitude: Attitude, 31 | gravity: CMAcceleration, 32 | heading: Double, 33 | magneticField: CMCalibratedMagneticField, 34 | rotationRate: CMRotationRate, 35 | timestamp: TimeInterval, 36 | userAcceleration: CMAcceleration 37 | ) { 38 | self.attitude = attitude 39 | self.gravity = gravity 40 | self.heading = heading 41 | self.magneticField = magneticField 42 | self.rotationRate = rotationRate 43 | self.timestamp = timestamp 44 | self.userAcceleration = userAcceleration 45 | } 46 | 47 | public static func == (lhs: Self, rhs: Self) -> Bool { 48 | lhs.attitude == rhs.attitude 49 | && lhs.gravity.x == rhs.gravity.x 50 | && lhs.gravity.y == rhs.gravity.y 51 | && lhs.gravity.z == rhs.gravity.z 52 | && lhs.heading == rhs.heading 53 | && lhs.magneticField.accuracy == rhs.magneticField.accuracy 54 | && lhs.magneticField.field.x == rhs.magneticField.field.x 55 | && lhs.magneticField.field.y == rhs.magneticField.field.y 56 | && lhs.magneticField.field.z == rhs.magneticField.field.z 57 | && lhs.rotationRate.x == rhs.rotationRate.x 58 | && lhs.rotationRate.y == rhs.rotationRate.y 59 | && lhs.rotationRate.z == rhs.rotationRate.z 60 | && lhs.timestamp == rhs.timestamp 61 | && lhs.userAcceleration.x == rhs.userAcceleration.x 62 | && lhs.userAcceleration.y == rhs.userAcceleration.y 63 | && lhs.userAcceleration.z == rhs.userAcceleration.z 64 | } 65 | 66 | public func hash(into hasher: inout Hasher) { 67 | hasher.combine(attitude) 68 | hasher.combine(gravity.x) 69 | hasher.combine(gravity.y) 70 | hasher.combine(gravity.z) 71 | hasher.combine(heading) 72 | hasher.combine(magneticField.accuracy) 73 | hasher.combine(magneticField.field.x) 74 | hasher.combine(magneticField.field.y) 75 | hasher.combine(magneticField.field.z) 76 | hasher.combine(rotationRate.x) 77 | hasher.combine(rotationRate.y) 78 | hasher.combine(rotationRate.z) 79 | hasher.combine(timestamp) 80 | hasher.combine(userAcceleration.x) 81 | hasher.combine(userAcceleration.y) 82 | hasher.combine(userAcceleration.z) 83 | } 84 | } 85 | #endif 86 | -------------------------------------------------------------------------------- /Sources/ComposableCoreMotion/Models/GyroData.swift: -------------------------------------------------------------------------------- 1 | // GyroData.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | #if canImport(CoreMotion) 5 | import CoreMotion 6 | 7 | /// A single measurement of the device's rotation rate. 8 | /// 9 | /// See the documentation for `CMGyroData` for more info. 10 | public struct GyroData: Hashable { 11 | public var rotationRate: CMRotationRate 12 | public var timestamp: TimeInterval 13 | 14 | public init(_ gyroData: CMGyroData) { 15 | rotationRate = gyroData.rotationRate 16 | timestamp = gyroData.timestamp 17 | } 18 | 19 | public init( 20 | rotationRate: CMRotationRate, 21 | timestamp: TimeInterval 22 | ) { 23 | self.rotationRate = rotationRate 24 | self.timestamp = timestamp 25 | } 26 | 27 | public static func == (lhs: Self, rhs: Self) -> Bool { 28 | lhs.rotationRate.x == rhs.rotationRate.x 29 | && lhs.rotationRate.y == rhs.rotationRate.y 30 | && lhs.rotationRate.z == rhs.rotationRate.z 31 | && lhs.timestamp == rhs.timestamp 32 | } 33 | 34 | public func hash(into hasher: inout Hasher) { 35 | hasher.combine(rotationRate.x) 36 | hasher.combine(rotationRate.y) 37 | hasher.combine(rotationRate.z) 38 | hasher.combine(timestamp) 39 | } 40 | } 41 | #endif 42 | -------------------------------------------------------------------------------- /Sources/ComposableCoreMotion/Models/MagnetometerData.swift: -------------------------------------------------------------------------------- 1 | // MagnetometerData.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | #if canImport(CoreMotion) 5 | import CoreMotion 6 | 7 | /// Measurements of the Earth's magnetic field relative to the device. 8 | /// 9 | /// See the documentation for `CMMagnetometerData` for more info. 10 | public struct MagnetometerData: Hashable { 11 | public var magneticField: CMMagneticField 12 | public var timestamp: TimeInterval 13 | 14 | public init(_ magnetometerData: CMMagnetometerData) { 15 | magneticField = magnetometerData.magneticField 16 | timestamp = magnetometerData.timestamp 17 | } 18 | 19 | public init( 20 | magneticField: CMMagneticField, 21 | timestamp: TimeInterval 22 | ) { 23 | self.magneticField = magneticField 24 | self.timestamp = timestamp 25 | } 26 | 27 | public static func == (lhs: Self, rhs: Self) -> Bool { 28 | lhs.magneticField.x == rhs.magneticField.x 29 | && lhs.magneticField.y == rhs.magneticField.y 30 | && lhs.magneticField.z == rhs.magneticField.z 31 | && lhs.timestamp == rhs.timestamp 32 | } 33 | 34 | public func hash(into hasher: inout Hasher) { 35 | hasher.combine(magneticField.x) 36 | hasher.combine(magneticField.y) 37 | hasher.combine(magneticField.z) 38 | hasher.combine(timestamp) 39 | } 40 | } 41 | #endif 42 | -------------------------------------------------------------------------------- /Sources/ComposableFast/Interface.swift: -------------------------------------------------------------------------------- 1 | // Interface.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | import ComposableArchitecture 5 | import Foundation 6 | import WebKit 7 | 8 | public struct FastManager { 9 | public enum Action: Equatable { 10 | case didReceive(message: WKScriptMessage) 11 | } 12 | 13 | var create: (AnyHashable) -> Effect = { _ in _unimplemented("create") } 14 | 15 | var destroy: (AnyHashable) -> Effect = { _ in _unimplemented("destroy") } 16 | 17 | var startTest: (AnyHashable) -> Effect = { _ in _unimplemented("startTest") } 18 | 19 | var stopTest: (AnyHashable) -> Effect = { _ in _unimplemented("stopTest") } 20 | 21 | public func create(id: AnyHashable, queue _: DispatchQueue? = nil, options _: [String: Any]? = nil) -> Effect { 22 | create(id) 23 | } 24 | 25 | public func destroy(id: AnyHashable) -> Effect { 26 | destroy(id) 27 | } 28 | 29 | public func startTest(id: AnyHashable) -> Effect { 30 | startTest(id) 31 | } 32 | 33 | public func stopTest(id: AnyHashable) -> Effect { 34 | stopTest(id) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/ComposableFast/Live.swift: -------------------------------------------------------------------------------- 1 | // Live.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | import Combine 5 | import ComposableArchitecture 6 | import Foundation 7 | import WebKit 8 | 9 | private let source = """ 10 | var speedTarget = document.querySelector('#speed-value'); 11 | var uploadTarget = document.querySelector('#upload-value'); 12 | 13 | var observer = new MutationObserver(function(mutations) { 14 | mutations.forEach(function(mutation) { 15 | 16 | if (mutation.target.id == "speed-value") { 17 | 18 | if (mutation.attributeName == "class") { 19 | window.webkit.messageHandlers.notification.postMessage({ type: "down-done", value: "", units: "" }); 20 | } else { 21 | let units = document.querySelector("#speed-units").innerText 22 | let value = mutation.addedNodes.item(0).data 23 | window.webkit.messageHandlers.notification.postMessage({ type: "down", value: value, units: units }); 24 | } 25 | } 26 | 27 | if (mutation.target.id == "upload-value") { 28 | if (mutation.attributeName == "class") { 29 | window.webkit.messageHandlers.notification.postMessage({ type: "up-done", value: "", units: "" }); 30 | } else { 31 | let units = document.querySelector("#upload-units").innerText 32 | let value = mutation.addedNodes.item(0).data 33 | window.webkit.messageHandlers.notification.postMessage({ type: "up", value: value, units: units }); 34 | } 35 | } 36 | }); 37 | }); 38 | 39 | var config = { attributes: true, childList: true, characterData: true } 40 | 41 | observer.observe(speedTarget, config); 42 | observer.observe(uploadTarget, config); 43 | """ 44 | 45 | public extension FastManager { 46 | static let live: FastManager = { () -> FastManager in 47 | var manager = FastManager() 48 | 49 | manager.create = { id in 50 | Effect.run { subscriber in 51 | let delegate = FastManagerDelegate(subscriber) 52 | let userContent = WKUserContentController() 53 | let config = WKWebViewConfiguration() 54 | let userScript = WKUserScript(source: source, 55 | injectionTime: .atDocumentEnd, 56 | forMainFrameOnly: true) 57 | userContent.addUserScript(userScript) 58 | userContent.add(delegate, name: "notification") 59 | 60 | config.userContentController = userContent 61 | let webview = WKWebView(frame: CGRect(x: 0, y: 0, width: 100, height: 100), 62 | configuration: config) 63 | 64 | dependencies[id] = Dependencies(delegate: delegate, 65 | userContent: userContent, 66 | config: config, 67 | webView: webview, 68 | subscriber: subscriber) 69 | return AnyCancellable { 70 | dependencies[id] = nil 71 | } 72 | } 73 | } 74 | 75 | manager.destroy = { id in 76 | .fireAndForget { 77 | dependencies[id]?.subscriber.send(completion: .finished) 78 | dependencies[id] = nil 79 | } 80 | } 81 | 82 | manager.startTest = { id in 83 | .fireAndForget { 84 | dependencies[id]?.webView.load(URLRequest(url: URL(string: "https://fast.com")!)) 85 | } 86 | } 87 | 88 | manager.stopTest = { id in 89 | .fireAndForget { 90 | dependencies[id]?.webView.stopLoading() 91 | } 92 | } 93 | return manager 94 | }() 95 | } 96 | 97 | // MARK: - Dependencies 98 | 99 | private struct Dependencies { 100 | let delegate: FastManagerDelegate 101 | let userContent: WKUserContentController 102 | let config: WKWebViewConfiguration 103 | let webView: WKWebView 104 | let subscriber: Effect.Subscriber 105 | } 106 | 107 | private var dependencies: [AnyHashable: Dependencies] = [:] 108 | 109 | // MARK: - Delegate 110 | 111 | private class FastManagerDelegate: NSObject, WKScriptMessageHandler { 112 | let subscriber: Effect.Subscriber 113 | 114 | init(_ subscriber: Effect.Subscriber) { 115 | self.subscriber = subscriber 116 | } 117 | 118 | func userContentController(_: WKUserContentController, didReceive message: WKScriptMessage) { 119 | subscriber.send(.didReceive(message: message)) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /Sources/ComposableFast/Mock.swift: -------------------------------------------------------------------------------- 1 | // Mock.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | #if DEBUG 5 | import Foundation 6 | 7 | public extension FastManager { 8 | static func mock() -> Self { Self() } 9 | } 10 | 11 | #endif 12 | 13 | // MARK: - Unimplemented 14 | 15 | public func _unimplemented( 16 | _ function: StaticString, file: StaticString = #file, line: UInt = #line 17 | ) -> Never { 18 | fatalError( 19 | """ 20 | `\(function)` was called but is not implemented. Be sure to provide an implementation for 21 | this endpoint when creating the mock. 22 | """, 23 | file: file, 24 | line: line 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /Sources/ComposableFast/README.md: -------------------------------------------------------------------------------- 1 | # ComposableFast 2 | 3 | A [Composable Architecture](https://github.com/pointfreeco/swift-composable-architecture) implementation of a fast.com speed test library. 4 | 5 | 6 | ### Setup 7 | 8 | Import 9 | ```swift 10 | import ComposableFast 11 | ``` 12 | 13 | Create Fast Manager id 14 | 15 | ```swift 16 | struct FastManagerId: Hashable {} 17 | ``` 18 | 19 | Add Fast Manager to AppEnvironment 20 | 21 | ```swift 22 | struct AppEnvironment { 23 | ... 24 | let fastManager: FastManager 25 | ... 26 | } 27 | ``` 28 | 29 | ### Life-cycle 30 | 31 | Create Fast Manager 32 | 33 | ```swift 34 | environment.fastManager.create(id: FastManagerId()).map(AppAction.fastManager) 35 | ``` 36 | 37 | Destroy Fast Manager 38 | 39 | ```swift 40 | environment.fastManager.destroy(id: FastManagerId()).fireAndForget(), 41 | ``` 42 | 43 | ### Operations 44 | 45 | Start Speed Test 46 | 47 | ```swift 48 | environment.fastManager.startTest(id: FastManagerId()).fireAndForget() 49 | ``` 50 | 51 | Stop Speed Test 52 | 53 | ```swift 54 | environment.fastManager.stopTest(id: FastManagerId()).fireAndForget() 55 | ``` 56 | 57 | ### Callbacks 58 | 59 | ```swift 60 | case let .didReceive(message: message): 61 | guard let body = message.body as? NSDictionary, 62 | let type = body["type"] as? String, 63 | let units = body["units"] as? String, 64 | let value = body["value"] as? String else { return .none } 65 | 66 | switch type { 67 | case "down": 68 | // handle download update 69 | return .none 70 | 71 | case "down-done": 72 | // handle download completion 73 | return .none 74 | 75 | case "up": 76 | // handle upload update 77 | return .none 78 | 79 | case "up-done": 80 | // handle upload completion 81 | default: 82 | return .none 83 | } 84 | } 85 | ``` -------------------------------------------------------------------------------- /Sources/ComposableHealthStore/Interface.swift: -------------------------------------------------------------------------------- 1 | // Interface.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | #if canImport(HealthKit) 5 | import ComposableArchitecture 6 | import Foundation 7 | import HealthKit 8 | 9 | public enum HealthStoreError: Error { 10 | case healthDataNotAvailable 11 | case authorizationError 12 | case startWatchAppFailure 13 | } 14 | 15 | public struct HealthStoreManager { 16 | public enum Action: Equatable {} 17 | 18 | public struct Error: Swift.Error, Equatable { 19 | public let error: NSError? 20 | 21 | public init(_ error: Swift.Error?) { 22 | self.error = error as NSError? 23 | } 24 | } 25 | 26 | // MARK: - Variables 27 | 28 | var create: (AnyHashable) -> Effect = { _ in _unimplemented("create") } 29 | 30 | var destroy: (AnyHashable) -> Effect = { _ in _unimplemented("destroy") } 31 | 32 | var requestAuthorization: (AnyHashable, Set?, Set?) -> Effect = { _, _, _ in _unimplemented("requestAuthorization") } 33 | 34 | var isHealthAuthorizedFor: (AnyHashable, Set, Set) -> Effect = { _, _, _ in _unimplemented("isHealthAuthorizedFor") } 35 | 36 | var startWatchApp: (AnyHashable, HKWorkoutConfiguration) -> Effect = { _, _ in _unimplemented("startWatchApp") } 37 | 38 | // MARK: - Functions 39 | 40 | public func create(id: AnyHashable) -> Effect { 41 | create(id) 42 | } 43 | 44 | public func destroy(id: AnyHashable) -> Effect { 45 | destroy(id) 46 | } 47 | 48 | public func requestAuthorization(id: AnyHashable, typesToShare: Set?, typesToRead: Set?) -> Effect { 49 | requestAuthorization(id, typesToShare, typesToRead) 50 | } 51 | 52 | public func isHealthAuthorizedFor(id: AnyHashable, typesToShare: Set, typesToRead: Set) -> Effect { 53 | isHealthAuthorizedFor(id, typesToShare, typesToRead) 54 | } 55 | 56 | public func startWatchApp(id: AnyHashable, configuration: HKWorkoutConfiguration) -> Effect { 57 | startWatchApp(id, configuration) 58 | } 59 | } 60 | #endif 61 | -------------------------------------------------------------------------------- /Sources/ComposableHealthStore/Live.swift: -------------------------------------------------------------------------------- 1 | // Live.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | #if canImport(HealthKit) 5 | import Combine 6 | import ComposableArchitecture 7 | import Foundation 8 | import HealthKit 9 | 10 | public extension HealthStoreManager { 11 | static let live: HealthStoreManager = { () -> HealthStoreManager in 12 | 13 | var manager = HealthStoreManager() 14 | 15 | manager.create = { id in 16 | Effect.run { subscriber in 17 | dependencies[id] = Dependencies( 18 | healthStore: HKHealthStore(), 19 | subscriber: subscriber 20 | ) 21 | 22 | return AnyCancellable { 23 | dependencies[id] = nil 24 | } 25 | } 26 | } 27 | 28 | manager.destroy = { id in 29 | .fireAndForget { 30 | dependencies[id]?.subscriber.send(completion: .finished) 31 | dependencies[id] = nil 32 | } 33 | } 34 | 35 | manager.requestAuthorization = { id, typesToShare, typeToRead in 36 | .fireAndForget { 37 | guard HKHealthStore.isHealthDataAvailable() else { return } 38 | dependencies[id]?.healthStore.requestAuthorization(toShare: typesToShare, read: typeToRead) { _, _ in 39 | } 40 | } 41 | } 42 | 43 | manager.isHealthAuthorizedFor = { id, typesToShare, typesToRead in 44 | .future { futureCompletion in 45 | dependencies[id]?.healthStore.getRequestStatusForAuthorization(toShare: typesToShare, read: typesToRead, completion: { status, error in 46 | switch error { 47 | case .some: 48 | break 49 | case .none: 50 | switch status { 51 | case .unnecessary: return futureCompletion(.success(true)) 52 | default: return futureCompletion(.success(false)) 53 | } 54 | } 55 | }) 56 | } 57 | } 58 | 59 | manager.startWatchApp = { id, configuration in 60 | .future { futureCompletion in 61 | dependencies[id]?.healthStore.startWatchApp(with: configuration, completion: { _, error in 62 | switch error { 63 | case .some: 64 | return futureCompletion(.success(false)) 65 | case .none: 66 | return futureCompletion(.success(true)) 67 | } 68 | }) 69 | } 70 | } 71 | 72 | return manager 73 | }() 74 | } 75 | 76 | private struct Dependencies { 77 | var healthStore: HKHealthStore 78 | let subscriber: Effect.Subscriber 79 | } 80 | 81 | private var dependencies: [AnyHashable: Dependencies] = [:] 82 | #endif 83 | -------------------------------------------------------------------------------- /Sources/ComposableHealthStore/Mock.swift: -------------------------------------------------------------------------------- 1 | // Mock.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | #if canImport(HealthKit) 5 | import ComposableArchitecture 6 | 7 | #if DEBUG 8 | public extension HealthStoreManager { 9 | static func unimplemented() -> Self { 10 | Self() 11 | } 12 | } 13 | 14 | #endif 15 | 16 | public func _unimplemented( 17 | _ function: StaticString, file: StaticString = #file, line: UInt = #line 18 | ) -> Never { 19 | fatalError( 20 | """ 21 | `\(function)` was called but is not implemented. Be sure to provide an implementation for 22 | this endpoint when creating the mock. 23 | """, 24 | file: file, 25 | line: line 26 | ) 27 | } 28 | #endif 29 | -------------------------------------------------------------------------------- /Sources/ComposableLocalSearch/Interface.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Joe Blau on 2/28/21. 6 | // 7 | 8 | import ComposableArchitecture 9 | import Foundation 10 | import MapKit 11 | 12 | public struct LocalSearchManager { 13 | 14 | public enum Action: Equatable { 15 | case completerDidUpdateResults(completer: MKLocalSearchCompleter) 16 | case completer(MKLocalSearchCompleter, didFailWithError: Error) 17 | } 18 | 19 | public struct Error: Swift.Error, Equatable { 20 | public let error: NSError? 21 | 22 | public init(_ error: Swift.Error?) { 23 | self.error = error as NSError? 24 | } 25 | } 26 | 27 | var create: (AnyHashable, MKLocalSearchCompleter.ResultType) -> Effect = { _, _ in _unimplemented("create") } 28 | 29 | var destroy: (AnyHashable) -> Effect = { _ in _unimplemented("destroy") } 30 | 31 | var query: (AnyHashable, String) -> Effect = { _, _ in _unimplemented("query") } 32 | 33 | public func create(id: AnyHashable, resultTypes: MKLocalSearchCompleter.ResultType) -> Effect { 34 | create(id, resultTypes) 35 | } 36 | 37 | public func destroy(id: AnyHashable) -> Effect { 38 | destroy(id) 39 | } 40 | 41 | public func query(id: AnyHashable, fragment: String) -> Effect { 42 | query(id, fragment) 43 | } 44 | } 45 | 46 | -------------------------------------------------------------------------------- /Sources/ComposableLocalSearch/Live.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Joe Blau on 2/28/21. 6 | // 7 | 8 | import Foundation 9 | 10 | import Combine 11 | import ComposableArchitecture 12 | import Foundation 13 | import MapKit 14 | 15 | public extension LocalSearchManager { 16 | static let live: LocalSearchManager = { () -> LocalSearchManager in 17 | 18 | var manager = LocalSearchManager() 19 | 20 | manager.create = { id, resultTypes in 21 | .run { subscriber in 22 | let localSearchCompleter = MKLocalSearchCompleter() 23 | let delegate = LocalSearchManagerDelegate(subscriber) 24 | localSearchCompleter.resultTypes = resultTypes 25 | localSearchCompleter.delegate = delegate 26 | 27 | dependencies[id] = Dependencies(delegate: delegate, 28 | localSearchCompleter: localSearchCompleter, 29 | subscriber: subscriber) 30 | return AnyCancellable { 31 | dependencies[id] = nil 32 | } 33 | } 34 | } 35 | 36 | manager.query = { id, fragment in 37 | .fireAndForget { 38 | dependencies[id]?.localSearchCompleter.queryFragment = fragment 39 | } 40 | } 41 | 42 | return manager 43 | }() 44 | } 45 | 46 | private struct Dependencies { 47 | let delegate: LocalSearchManagerDelegate 48 | let localSearchCompleter: MKLocalSearchCompleter 49 | let subscriber: Effect.Subscriber 50 | } 51 | 52 | 53 | private var dependencies: [AnyHashable: Dependencies] = [:] 54 | 55 | // MARK: - Delegate 56 | 57 | private class LocalSearchManagerDelegate: NSObject, MKLocalSearchCompleterDelegate { 58 | let subscriber: Effect.Subscriber 59 | 60 | init(_ subscriber: Effect.Subscriber) { 61 | self.subscriber = subscriber 62 | } 63 | 64 | func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) { 65 | subscriber.send(.completerDidUpdateResults(completer: completer)) 66 | } 67 | 68 | func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) { 69 | subscriber.send(.completer(completer, didFailWithError: LocalSearchManager.Error(error))) 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /Sources/ComposableLocalSearch/Mock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Joe Blau on 2/28/21. 6 | // 7 | 8 | #if DEBUG 9 | import Foundation 10 | 11 | public extension LocalSearchManager { 12 | static func unimplemented() -> Self { 13 | Self() 14 | } 15 | } 16 | 17 | #endif 18 | 19 | // MARK: - Unimplemented 20 | 21 | public func _unimplemented( 22 | _ function: StaticString, file: StaticString = #file, line: UInt = #line 23 | ) -> Never { 24 | fatalError( 25 | """ 26 | `\(function)` was called but is not implemented. Be sure to provide an implementation for 27 | this endpoint when creating the mock. 28 | """, 29 | file: file, 30 | line: line 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /Sources/ComposableMediaPlayer/Interface.swift: -------------------------------------------------------------------------------- 1 | // Interface.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | #if canImport(MediaPlayer) && os(iOS) 5 | import ComposableArchitecture 6 | import Foundation 7 | import MediaPlayer 8 | 9 | public struct MediaPlayerManager { 10 | public enum Action: Equatable { 11 | case nowPlayingItemDidChange 12 | case playbackStateDidChange 13 | } 14 | 15 | public struct Error: Swift.Error, Equatable { 16 | public let error: NSError? 17 | 18 | public init(_ error: Swift.Error?) { 19 | self.error = error as NSError? 20 | } 21 | } 22 | 23 | // MARK: - Variables 24 | 25 | var create: (AnyHashable) -> Effect = { _ in _unimplemented("create") } 26 | 27 | var destroy: (AnyHashable) -> Effect = { _ in _unimplemented("destroy") } 28 | 29 | // MARK: - Functions 30 | 31 | public func create(id: AnyHashable) -> Effect { 32 | create(id) 33 | } 34 | 35 | public func destroy(id: AnyHashable) -> Effect { 36 | destroy(id) 37 | } 38 | } 39 | #endif 40 | -------------------------------------------------------------------------------- /Sources/ComposableMediaPlayer/Live.swift: -------------------------------------------------------------------------------- 1 | // Live.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | #if canImport(MediaPlayer) && os(iOS) 5 | import Combine 6 | import ComposableArchitecture 7 | import Foundation 8 | import MediaPlayer 9 | 10 | public extension MediaPlayerManager { 11 | static let live: MediaPlayerManager = { () -> MediaPlayerManager in 12 | 13 | var manager = MediaPlayerManager() 14 | 15 | manager.create = { id in 16 | 17 | Effect.run { subscriber in 18 | let systemMediaPlayer = MPMusicPlayerController.systemMusicPlayer 19 | let notificationCenter = NotificationCenter.default 20 | let delegate = WatchConnectivityDelegate(subscriber) 21 | 22 | notificationCenter.addObserver(delegate, 23 | selector: #selector(delegate.handleMusicPlayerControllerNowPlayingItemDidChange), 24 | name: .MPMusicPlayerControllerNowPlayingItemDidChange, 25 | object: systemMediaPlayer) 26 | 27 | notificationCenter.addObserver(delegate, 28 | selector: #selector(delegate.handleMusicPlayerControllerPlaybackStateDidChange), 29 | name: .MPMusicPlayerControllerPlaybackStateDidChange, 30 | object: systemMediaPlayer) 31 | 32 | dependencies[id] = Dependencies( 33 | systemMediaPlayer: systemMediaPlayer, 34 | notificationCenter: notificationCenter, 35 | delegate: delegate, 36 | subscriber: subscriber 37 | ) 38 | 39 | systemMediaPlayer.beginGeneratingPlaybackNotifications() 40 | 41 | return AnyCancellable { 42 | dependencies[id] = nil 43 | } 44 | } 45 | } 46 | 47 | manager.destroy = { id in 48 | .fireAndForget { 49 | dependencies[id]?.systemMediaPlayer.endGeneratingPlaybackNotifications() 50 | dependencies[id]?.subscriber.send(completion: .finished) 51 | dependencies[id] = nil 52 | } 53 | } 54 | 55 | return manager 56 | }() 57 | } 58 | 59 | private struct Dependencies { 60 | var systemMediaPlayer: MPMusicPlayerController 61 | var notificationCenter: NotificationCenter 62 | let delegate: WatchConnectivityDelegate 63 | let subscriber: Effect.Subscriber 64 | } 65 | 66 | private var dependencies: [AnyHashable: Dependencies] = [:] 67 | 68 | private class WatchConnectivityDelegate: NSObject { 69 | let subscriber: Effect.Subscriber 70 | 71 | init(_ subscriber: Effect.Subscriber) { 72 | self.subscriber = subscriber 73 | } 74 | 75 | @objc func handleMusicPlayerControllerNowPlayingItemDidChange() { 76 | subscriber.send(.nowPlayingItemDidChange) 77 | } 78 | 79 | @objc func handleMusicPlayerControllerPlaybackStateDidChange() { 80 | subscriber.send(.playbackStateDidChange) 81 | } 82 | } 83 | #endif 84 | -------------------------------------------------------------------------------- /Sources/ComposableMediaPlayer/Mock.swift: -------------------------------------------------------------------------------- 1 | // Mock.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | #if canImport(MediaPlayer) && os(iOS) 5 | import ComposableArchitecture 6 | 7 | #if DEBUG 8 | public extension MediaPlayerManager { 9 | static func unimplemented() -> Self { 10 | Self() 11 | } 12 | } 13 | 14 | #endif 15 | 16 | public func _unimplemented( 17 | _ function: StaticString, file: StaticString = #file, line: UInt = #line 18 | ) -> Never { 19 | fatalError( 20 | """ 21 | `\(function)` was called but is not implemented. Be sure to provide an implementation for 22 | this endpoint when creating the mock. 23 | """, 24 | file: file, 25 | line: line 26 | ) 27 | } 28 | #endif 29 | -------------------------------------------------------------------------------- /Sources/ComposablePlayer/Interface.swift: -------------------------------------------------------------------------------- 1 | // Interface.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | import AVFoundation 5 | import ComposableArchitecture 6 | 7 | public struct AudioPlayer { 8 | public enum Action: Equatable {} 9 | 10 | public struct Error: Swift.Error, Equatable { 11 | public let error: NSError? 12 | 13 | public init(_ error: Swift.Error?) { 14 | self.error = error as NSError? 15 | } 16 | } 17 | 18 | // MARK: - Variables 19 | 20 | var create: (AnyHashable) -> Effect = { _ in _unimplemented("create") } 21 | 22 | var destroy: (AnyHashable) -> Effect = { _ in _unimplemented("destroy") } 23 | 24 | var play: (AnyHashable, URL) -> Effect = { _, _ in _unimplemented("play") } 25 | 26 | var pause: (AnyHashable) -> Effect = { _ in _unimplemented("stop") } 27 | 28 | // MARK: - Functions 29 | 30 | public func create(id: AnyHashable) -> Effect { 31 | create(id) 32 | } 33 | 34 | public func destroy(id: AnyHashable) -> Effect { 35 | destroy(id) 36 | } 37 | 38 | func play(id: AnyHashable, url: URL) -> Effect { 39 | play(id, url) 40 | } 41 | 42 | func pause(id: AnyHashable) -> Effect { 43 | pause(id) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/ComposablePlayer/Live.swift: -------------------------------------------------------------------------------- 1 | // Live.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | import AVFoundation 5 | import Combine 6 | import ComposableArchitecture 7 | import Foundation 8 | 9 | public extension AudioPlayer { 10 | static let live: AudioPlayer = { () -> AudioPlayer in 11 | 12 | var manager = AudioPlayer() 13 | 14 | manager.create = { id in 15 | 16 | Effect.run { subscriber in 17 | let player = AVPlayer() 18 | 19 | dependencies[id] = Dependencies( 20 | player: player, 21 | subscriber: subscriber, 22 | queue: OperationQueue.main 23 | ) 24 | 25 | return AnyCancellable { 26 | dependencies[id] = nil 27 | } 28 | } 29 | } 30 | 31 | manager.destroy = { id in 32 | .fireAndForget { 33 | dependencies[id]?.subscriber.send(completion: .finished) 34 | dependencies[id] = nil 35 | } 36 | } 37 | 38 | manager.play = { id, url in 39 | .fireAndForget { 40 | dependencies[id]?.player = AVPlayer(url: url) 41 | dependencies[id]?.player.play() 42 | } 43 | } 44 | 45 | manager.pause = { id in 46 | .fireAndForget { dependencies[id]?.player.pause() } 47 | } 48 | 49 | return manager 50 | }() 51 | } 52 | 53 | private struct Dependencies { 54 | var player: AVPlayer 55 | let subscriber: Effect.Subscriber 56 | let queue: OperationQueue 57 | } 58 | 59 | private var dependencies: [AnyHashable: Dependencies] = [:] 60 | -------------------------------------------------------------------------------- /Sources/ComposablePlayer/Mock.swift: -------------------------------------------------------------------------------- 1 | // Mock.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | import Foundation 5 | 6 | #if DEBUG 7 | public extension AudioPlayer { 8 | static func mock() -> Self { Self() } 9 | } 10 | 11 | #endif 12 | 13 | public func _unimplemented( 14 | _ function: StaticString, file: StaticString = #file, line: UInt = #line 15 | ) -> Never { 16 | fatalError( 17 | """ 18 | `\(function)` was called but is not implemented. Be sure to provide an implementation for 19 | this endpoint when creating the mock. 20 | """, 21 | file: file, 22 | line: line 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /Sources/ComposableSpeechRecognizer/Interface.swift: -------------------------------------------------------------------------------- 1 | // Interface.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | import ComposableArchitecture 5 | import Foundation 6 | import Speech 7 | 8 | public struct SpeechRecognizerManager { 9 | public enum Action: Equatable { 10 | case availiblityDidChange(Bool) 11 | case speechRecognitionResult(SFSpeechRecognitionResult?) 12 | case authorizationStatus(SFSpeechRecognizerAuthorizationStatus) 13 | } 14 | 15 | var create: (AnyHashable) -> Effect = { _ in _unimplemented("create") } 16 | 17 | var destroy: (AnyHashable) -> Effect = { _ in _unimplemented("destroy") } 18 | 19 | @available(macOS, unavailable) 20 | var requestAuthorization: (AnyHashable) -> Effect = { _ in _unimplemented("requestAuthorization") } 21 | 22 | @available(macOS, unavailable) 23 | var startListening: (AnyHashable) -> Effect = { _ in _unimplemented("startListening") } 24 | 25 | @available(macOS, unavailable) 26 | var stopListening: (AnyHashable) -> Effect = { _ in _unimplemented("stopListening") } 27 | 28 | public func create(id: AnyHashable, queue _: DispatchQueue? = nil, options _: [String: Any]? = nil) -> Effect { 29 | create(id) 30 | } 31 | 32 | public func destroy(id: AnyHashable) -> Effect { 33 | destroy(id) 34 | } 35 | 36 | @available(macOS, unavailable) 37 | public func requestAuthorization(id: AnyHashable) -> Effect { 38 | requestAuthorization(id) 39 | } 40 | 41 | @available(macOS, unavailable) 42 | public func startListening(id: AnyHashable) -> Effect { 43 | startListening(id) 44 | } 45 | 46 | @available(macOS, unavailable) 47 | public func stopListening(id: AnyHashable) -> Effect { 48 | stopListening(id) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/ComposableSpeechRecognizer/Live.swift: -------------------------------------------------------------------------------- 1 | // Live.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | import Combine 5 | import ComposableArchitecture 6 | import Foundation 7 | import os.log 8 | import Speech 9 | 10 | public extension SpeechRecognizerManager { 11 | static let live: SpeechRecognizerManager = { () -> SpeechRecognizerManager in 12 | var manager = SpeechRecognizerManager() 13 | 14 | manager.create = { id in 15 | Effect.run { subscriber in 16 | let delegate = SpeechSpeechRecognizerDelegate(subscriber) 17 | let speechRecognizer = SFSpeechRecognizer(locale: Locale(identifier: "en-US"))! 18 | speechRecognizer.delegate = delegate 19 | 20 | dependencies[id] = Dependencies(delegate: delegate, 21 | speechRecognizer: speechRecognizer, 22 | recognitionRequest: nil, 23 | recognitionTask: nil, 24 | audioEngine: AVAudioEngine(), 25 | subscriber: subscriber) 26 | return AnyCancellable { 27 | dependencies[id] = nil 28 | } 29 | } 30 | } 31 | 32 | manager.destroy = { id in 33 | .fireAndForget { 34 | dependencies[id]?.subscriber.send(completion: .finished) 35 | dependencies[id] = nil 36 | } 37 | } 38 | 39 | #if os(iOS) || os(watchOS) || targetEnvironment(macCatalyst) 40 | manager.requestAuthorization = { id in 41 | .fireAndForget { 42 | SFSpeechRecognizer.requestAuthorization { authStatus in 43 | dependencies[id]?.subscriber.send(.authorizationStatus(authStatus)) 44 | } 45 | } 46 | } 47 | #endif 48 | 49 | #if os(iOS) || os(watchOS) || targetEnvironment(macCatalyst) 50 | manager.startListening = { id in 51 | .future { _ in 52 | 53 | do { 54 | dependencies[id]?.recognitionTask?.cancel() 55 | dependencies[id]?.recognitionTask = nil 56 | 57 | // Configure the audio session for the app. 58 | try AVAudioSession.sharedInstance().setCategory(.record, mode: .measurement, options: .duckOthers) 59 | try AVAudioSession.sharedInstance().setActive(true, options: .notifyOthersOnDeactivation) 60 | let inputNode = dependencies[id]?.audioEngine.inputNode 61 | 62 | // Create and configure the speech recognition request. 63 | dependencies[id]?.recognitionRequest = SFSpeechAudioBufferRecognitionRequest() 64 | guard let recognitionRequest = dependencies[id]?.recognitionRequest else { fatalError("Unable to create a SFSpeechAudioBufferRecognitionRequest object") } 65 | recognitionRequest.shouldReportPartialResults = true 66 | 67 | // Keep speech recognition data on device 68 | recognitionRequest.requiresOnDeviceRecognition = false 69 | 70 | // Create a recognition task for the speech recognition session. 71 | // Keep a reference to the task so that it can be canceled. 72 | dependencies[id]!.recognitionTask = dependencies[id]?.speechRecognizer.recognitionTask(with: recognitionRequest) { result, error in 73 | dependencies[id]?.subscriber.send(.speechRecognitionResult(result)) 74 | 75 | guard let isFinal = result?.isFinal, 76 | error == nil, isFinal else { return } 77 | 78 | // Stop recognizing speech if there is a problem. 79 | dependencies[id]?.audioEngine.stop() 80 | inputNode?.removeTap(onBus: 0) 81 | 82 | dependencies[id]?.recognitionRequest = nil 83 | dependencies[id]?.recognitionTask = nil 84 | } 85 | 86 | // Configure the microphone input. 87 | let recordingFormat = inputNode?.outputFormat(forBus: 0) 88 | inputNode?.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { (buffer: AVAudioPCMBuffer, _: AVAudioTime) in 89 | dependencies[id]?.recognitionRequest?.append(buffer) 90 | } 91 | 92 | dependencies[id]?.audioEngine.prepare() 93 | try dependencies[id]?.audioEngine.start() 94 | } catch { 95 | os_log("%@", error.localizedDescription) 96 | } 97 | } 98 | } 99 | #endif 100 | 101 | #if os(iOS) || os(watchOS) || targetEnvironment(macCatalyst) 102 | manager.stopListening = { id in 103 | .fireAndForget { 104 | dependencies[id]?.audioEngine.stop() 105 | dependencies[id]?.audioEngine.inputNode.removeTap(onBus: 0) 106 | dependencies[id]?.recognitionRequest?.endAudio() 107 | dependencies[id]?.recognitionTask?.cancel() 108 | } 109 | } 110 | #endif 111 | 112 | return manager 113 | }() 114 | } 115 | 116 | // MARK: - Dependencies 117 | 118 | private struct Dependencies { 119 | let delegate: SFSpeechRecognizerDelegate 120 | let speechRecognizer: SFSpeechRecognizer 121 | var recognitionRequest: SFSpeechAudioBufferRecognitionRequest? 122 | var recognitionTask: SFSpeechRecognitionTask? 123 | let audioEngine: AVAudioEngine 124 | let subscriber: Effect.Subscriber 125 | } 126 | 127 | private var dependencies: [AnyHashable: Dependencies] = [:] 128 | 129 | // MARK: - Delegate 130 | 131 | private class SpeechSpeechRecognizerDelegate: NSObject, SFSpeechRecognizerDelegate { 132 | let subscriber: Effect.Subscriber 133 | 134 | init(_ subscriber: Effect.Subscriber) { 135 | self.subscriber = subscriber 136 | } 137 | 138 | func speechRecognizer(_: SFSpeechRecognizer, availabilityDidChange available: Bool) { 139 | subscriber.send(.availiblityDidChange(available)) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /Sources/ComposableSpeechRecognizer/Mock.swift: -------------------------------------------------------------------------------- 1 | // Mock.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | import Foundation 5 | 6 | #if DEBUG 7 | public extension SpeechRecognizerManager { 8 | static func mock() -> Self { Self() } 9 | } 10 | 11 | #endif 12 | 13 | // MARK: - Unimplemented 14 | 15 | public func _unimplemented( 16 | _ function: StaticString, file: StaticString = #file, line: UInt = #line 17 | ) -> Never { 18 | fatalError( 19 | """ 20 | `\(function)` was called but is not implemented. Be sure to provide an implementation for 21 | this endpoint when creating the mock. 22 | """, 23 | file: file, 24 | line: line 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /Sources/ComposableSpeechSynthesizer/Interface.swift: -------------------------------------------------------------------------------- 1 | // Interface.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | import AVFoundation 5 | import ComposableArchitecture 6 | // 7 | // Interface.swift 8 | // Check 9 | // 10 | // Created by Joe Blau on 10/4/20. 11 | // 12 | import Foundation 13 | 14 | public struct SpeechSynthesizerManager { 15 | public enum Action: Equatable { 16 | case didContinue(utterance: AVSpeechUtterance) 17 | case didFinsih(utterance: AVSpeechUtterance) 18 | case didPause(utterance: AVSpeechUtterance) 19 | case didStart(utterance: AVSpeechUtterance) 20 | case willSepakRangeOfSpeachString(NSRange, utterance: AVSpeechUtterance) 21 | } 22 | 23 | var create: (AnyHashable) -> Effect = { _ in _unimplemented("create") } 24 | 25 | var destroy: (AnyHashable) -> Effect = { _ in _unimplemented("destroy") } 26 | 27 | @available(macOS, unavailable) 28 | var speak: (AnyHashable, AVSpeechUtterance) -> Effect = { _, _ in _unimplemented("speak") } 29 | 30 | @available(macOS, unavailable) 31 | var continueSpeaking: (AnyHashable) -> Effect = { _ in _unimplemented("continueSpeaking") } 32 | 33 | @available(macOS, unavailable) 34 | var pauseSpeaking: (AnyHashable, AVSpeechBoundary) -> Effect = { _, _ in _unimplemented("pauseSpeaking") } 35 | 36 | @available(macOS, unavailable) 37 | public var isPaused: (AnyHashable) -> Bool? = { _ in _unimplemented("isPaused") } 38 | 39 | @available(macOS, unavailable) 40 | public var isSpeaking: (AnyHashable) -> Bool? = { _ in _unimplemented("isSpeaking") } 41 | 42 | @available(macOS, unavailable) 43 | var stopSpeaking: (AnyHashable, AVSpeechBoundary) -> Effect = { _, _ in _unimplemented("stopSpeaking") } 44 | 45 | public func create(id: AnyHashable, queue _: DispatchQueue? = nil, options _: [String: Any]? = nil) -> Effect { 46 | create(id) 47 | } 48 | 49 | public func destroy(id: AnyHashable) -> Effect { 50 | destroy(id) 51 | } 52 | 53 | @available(macOS, unavailable) 54 | public func speak(id: AnyHashable, utterance: AVSpeechUtterance) -> Effect { 55 | speak(id, utterance) 56 | } 57 | 58 | @available(macOS, unavailable) 59 | public func continueSpeaking(id: AnyHashable) -> Effect { 60 | continueSpeaking(id) 61 | } 62 | 63 | @available(macOS, unavailable) 64 | public func pauseSpeaking(id: AnyHashable, at: AVSpeechBoundary) -> Effect { 65 | pauseSpeaking(id, at) 66 | } 67 | 68 | @available(macOS, unavailable) 69 | public func isPaused(id: AnyHashable) -> Bool? { 70 | isPaused(id) 71 | } 72 | 73 | @available(macOS, unavailable) 74 | public func isSpeaking(id: AnyHashable) -> Bool? { 75 | isSpeaking(id) 76 | } 77 | 78 | @available(macOS, unavailable) 79 | public func stopSpeaking(id: AnyHashable, at: AVSpeechBoundary) -> Effect { 80 | stopSpeaking(id, at) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Sources/ComposableSpeechSynthesizer/Live.swift: -------------------------------------------------------------------------------- 1 | // Live.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | import AVFoundation 5 | import Combine 6 | import ComposableArchitecture 7 | import Foundation 8 | import os.log 9 | 10 | public extension SpeechSynthesizerManager { 11 | static let live: SpeechSynthesizerManager = { () -> SpeechSynthesizerManager in 12 | var manager = SpeechSynthesizerManager() 13 | 14 | manager.create = { id in 15 | Effect.run { subscriber in 16 | let delegate = SpeechSynthesizerManagerDelegate(subscriber) 17 | let speechSynthesizer = AVSpeechSynthesizer() 18 | speechSynthesizer.delegate = delegate 19 | 20 | dependencies[id] = Dependencies(delegate: delegate, 21 | speechSynthesizer: speechSynthesizer, 22 | subscriber: subscriber) 23 | return AnyCancellable { 24 | dependencies[id] = nil 25 | } 26 | } 27 | } 28 | 29 | manager.destroy = { id in 30 | .fireAndForget { 31 | dependencies[id]?.subscriber.send(completion: .finished) 32 | dependencies[id] = nil 33 | } 34 | } 35 | 36 | #if os(iOS) || os(watchOS) || targetEnvironment(macCatalyst) 37 | manager.speak = { id, utterance in 38 | .fireAndForget { 39 | do { 40 | try AVAudioSession.sharedInstance().setCategory(.playback, mode: .spokenAudio, options: .mixWithOthers) 41 | dependencies[id]?.speechSynthesizer.speak(utterance) 42 | } catch { 43 | os_log("%@", error.localizedDescription) 44 | } 45 | } 46 | } 47 | #endif 48 | 49 | #if os(iOS) || os(watchOS) || targetEnvironment(macCatalyst) 50 | manager.continueSpeaking = { id in 51 | .fireAndForget { 52 | do { 53 | try AVAudioSession.sharedInstance().setCategory(.playback, mode: .spokenAudio, options: .mixWithOthers) 54 | dependencies[id]?.speechSynthesizer.continueSpeaking() 55 | } catch { 56 | os_log("%@", error.localizedDescription) 57 | } 58 | } 59 | } 60 | #endif 61 | 62 | #if os(iOS) || os(watchOS) || targetEnvironment(macCatalyst) 63 | manager.pauseSpeaking = { id, boundary in 64 | .fireAndForget { 65 | dependencies[id]?.speechSynthesizer.pauseSpeaking(at: boundary) 66 | } 67 | } 68 | #endif 69 | 70 | #if os(iOS) || os(watchOS) || targetEnvironment(macCatalyst) 71 | manager.isPaused = { id in 72 | dependencies[id]?.speechSynthesizer.isPaused 73 | } 74 | #endif 75 | 76 | #if os(iOS) || os(watchOS) || targetEnvironment(macCatalyst) 77 | manager.isSpeaking = { id in 78 | dependencies[id]?.speechSynthesizer.isSpeaking 79 | } 80 | #endif 81 | 82 | #if os(iOS) || os(watchOS) || targetEnvironment(macCatalyst) 83 | manager.stopSpeaking = { id, boundary in 84 | .fireAndForget { 85 | dependencies[id]?.speechSynthesizer.stopSpeaking(at: boundary) 86 | } 87 | } 88 | #endif 89 | 90 | return manager 91 | }() 92 | } 93 | 94 | // MARK: - Dependencies 95 | 96 | private struct Dependencies { 97 | let delegate: AVSpeechSynthesizerDelegate 98 | let speechSynthesizer: AVSpeechSynthesizer 99 | let subscriber: Effect.Subscriber 100 | } 101 | 102 | private var dependencies: [AnyHashable: Dependencies] = [:] 103 | 104 | // MARK: - Delegate 105 | 106 | private class SpeechSynthesizerManagerDelegate: NSObject, AVSpeechSynthesizerDelegate { 107 | let subscriber: Effect.Subscriber 108 | 109 | init(_ subscriber: Effect.Subscriber) { 110 | self.subscriber = subscriber 111 | } 112 | 113 | func speechSynthesizer(_: AVSpeechSynthesizer, didContinue utterance: AVSpeechUtterance) { 114 | subscriber.send(.didContinue(utterance: utterance)) 115 | } 116 | 117 | func speechSynthesizer(_: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) { 118 | subscriber.send(.didFinsih(utterance: utterance)) 119 | } 120 | 121 | func speechSynthesizer(_: AVSpeechSynthesizer, didPause utterance: AVSpeechUtterance) { 122 | subscriber.send(.didPause(utterance: utterance)) 123 | } 124 | 125 | func speechSynthesizer(_: AVSpeechSynthesizer, didStart utterance: AVSpeechUtterance) { 126 | subscriber.send(.didStart(utterance: utterance)) 127 | } 128 | 129 | func speechSynthesizer(_: AVSpeechSynthesizer, willSpeakRangeOfSpeechString characterRange: NSRange, utterance: AVSpeechUtterance) { 130 | subscriber.send(.willSepakRangeOfSpeachString(characterRange, utterance: utterance)) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /Sources/ComposableSpeechSynthesizer/Mock.swift: -------------------------------------------------------------------------------- 1 | // Mock.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | import Foundation 5 | 6 | #if DEBUG 7 | public extension SpeechSynthesizerManager { 8 | static func mock() -> Self { Self() } 9 | } 10 | 11 | #endif 12 | 13 | // MARK: - Unimplemented 14 | 15 | public func _unimplemented( 16 | _ function: StaticString, file: StaticString = #file, line: UInt = #line 17 | ) -> Never { 18 | fatalError( 19 | """ 20 | `\(function)` was called but is not implemented. Be sure to provide an implementation for 21 | this endpoint when creating the mock. 22 | """, 23 | file: file, 24 | line: line 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /Sources/ComposableWatchConnectivity/Extensions/WCSessionActivationState+Extensions.swift: -------------------------------------------------------------------------------- 1 | // WCSessionActivationState+Extensions.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | #if canImport(WatchConnectivity) 5 | import Foundation 6 | import WatchConnectivity 7 | 8 | extension WCSessionActivationState: CustomStringConvertible { 9 | public var description: String { 10 | switch self { 11 | case .notActivated: return "Not Activated" 12 | case .inactive: return "Inactive" 13 | case .activated: return "Activated" 14 | @unknown default: return "Unknown" 15 | } 16 | } 17 | } 18 | #endif 19 | -------------------------------------------------------------------------------- /Sources/ComposableWatchConnectivity/Interface.swift: -------------------------------------------------------------------------------- 1 | // Interface.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | #if canImport(WatchConnectivity) 5 | import ComposableArchitecture 6 | import Foundation 7 | import WatchConnectivity 8 | 9 | public struct WatchConnectivityManager { 10 | public enum Action: Equatable { 11 | case activationDidCompleteWith(activationState: WCSessionActivationState, error: Error?) 12 | case didReceiveMessage(message: NSDictionary) 13 | case didReceiveApplicationContext(applicationContext: NSDictionary) 14 | 15 | #if os(iOS) 16 | case sessionDidBecomeInactive 17 | case sessionDidDeactivate 18 | case sessionWatchStateDidChange 19 | case sessionReachabilityDidChange 20 | #endif 21 | } 22 | 23 | public struct Error: Swift.Error, Equatable { 24 | public let error: NSError? 25 | 26 | public init(_ error: Swift.Error?) { 27 | self.error = error as NSError? 28 | } 29 | } 30 | 31 | // MARK: - Variables 32 | 33 | var create: (AnyHashable) -> Effect = { _ in _unimplemented("create") } 34 | 35 | var destroy: (AnyHashable) -> Effect = { _ in _unimplemented("destroy") } 36 | 37 | var updateApplicationContext: (AnyHashable, [String: Any]) -> Effect = { _, _ in _unimplemented("updateApplicationContext") } 38 | 39 | var sendMessage: (AnyHashable, [String: Any], (([String: Any]) -> Void)?) -> Effect = { _, _, _ in _unimplemented("sendMessage") } 40 | 41 | var isReachable: (AnyHashable) -> Bool = { _ in _unimplemented("isReachable") } 42 | 43 | var isPaired: (AnyHashable) -> Bool = { _ in _unimplemented("isPaired") } 44 | 45 | var isWatchAppInstalled: (AnyHashable) -> Bool = { _ in _unimplemented("isWatchAppInstalled") } 46 | 47 | var validReachableSession: (AnyHashable) -> Bool = { _ in _unimplemented("validReachableSession") } 48 | 49 | // MARK: - Functions 50 | 51 | public func create(id: AnyHashable) -> Effect { 52 | create(id) 53 | } 54 | 55 | public func destroy(id: AnyHashable) -> Effect { 56 | destroy(id) 57 | } 58 | 59 | public func updateApplicationContext(id: AnyHashable, applicationContext: [String: Any]) -> Effect { 60 | updateApplicationContext(id, applicationContext) 61 | } 62 | 63 | public func sendMessage(id: AnyHashable, message: [String: Any], replyHandler: (([String: Any]) -> Void)?) -> Effect { 64 | sendMessage(id, message, replyHandler) 65 | } 66 | 67 | public func isReachable(id: AnyHashable) -> Bool { 68 | isReachable(id) 69 | } 70 | 71 | #if os(iOS) 72 | public func isPaired(id: AnyHashable) -> Bool { 73 | isPaired(id) 74 | } 75 | 76 | public func isWatchAppInstalled(id: AnyHashable) -> Bool { 77 | isWatchAppInstalled(id) 78 | } 79 | #endif 80 | 81 | public func validReachableSession(id: AnyHashable) -> Bool { 82 | validReachableSession(id) 83 | } 84 | } 85 | #endif 86 | -------------------------------------------------------------------------------- /Sources/ComposableWatchConnectivity/Live.swift: -------------------------------------------------------------------------------- 1 | // Live.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | #if canImport(WatchConnectivity) 5 | import Combine 6 | import ComposableArchitecture 7 | import Foundation 8 | import WatchConnectivity 9 | 10 | public extension WatchConnectivityManager { 11 | static let live: WatchConnectivityManager = { () -> WatchConnectivityManager in 12 | 13 | var manager = WatchConnectivityManager() 14 | 15 | manager.create = { id in 16 | 17 | Effect.run { subscriber in 18 | guard WCSession.isSupported() else { 19 | return AnyCancellable { 20 | dependencies[id] = nil 21 | } 22 | } 23 | 24 | var delegate = WatchConnectivityDelegate(subscriber) 25 | let session = WCSession.default 26 | session.delegate = delegate 27 | session.activate() 28 | 29 | dependencies[id] = Dependencies( 30 | session: session, 31 | watchConnectivityDelegate: delegate, 32 | subscriber: subscriber 33 | ) 34 | 35 | return AnyCancellable { 36 | dependencies[id] = nil 37 | } 38 | } 39 | } 40 | 41 | manager.destroy = { id in 42 | .fireAndForget { 43 | dependencies[id]?.subscriber.send(completion: .finished) 44 | dependencies[id] = nil 45 | } 46 | } 47 | 48 | manager.updateApplicationContext = { id, applicationContext in 49 | .fireAndForget { 50 | try? dependencies[id]?.session.updateApplicationContext(applicationContext) 51 | } 52 | } 53 | 54 | manager.sendMessage = { id, message, replyHandler in 55 | .fireAndForget { 56 | dependencies[id]?.session.sendMessage(message, replyHandler: replyHandler) 57 | } 58 | } 59 | 60 | manager.isReachable = { id in 61 | dependencies[id]?.session.isReachable ?? false 62 | } 63 | 64 | #if os(iOS) 65 | manager.isPaired = { id in 66 | dependencies[id]?.session.isPaired ?? false 67 | } 68 | 69 | manager.isWatchAppInstalled = { id in 70 | dependencies[id]?.session.isWatchAppInstalled ?? false 71 | } 72 | #endif 73 | 74 | return manager 75 | }() 76 | } 77 | 78 | private struct Dependencies { 79 | var session: WCSession 80 | let watchConnectivityDelegate: WatchConnectivityDelegate 81 | let subscriber: Effect.Subscriber 82 | } 83 | 84 | private var dependencies: [AnyHashable: Dependencies] = [:] 85 | 86 | // MARK: - WCSessionDelegate 87 | 88 | private class WatchConnectivityDelegate: NSObject, WCSessionDelegate { 89 | let subscriber: Effect.Subscriber 90 | 91 | init(_ subscriber: Effect.Subscriber) { 92 | self.subscriber = subscriber 93 | } 94 | 95 | func session(_: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { 96 | DispatchQueue.main.async { 97 | self.subscriber.send(.activationDidCompleteWith(activationState: activationState, error: WatchConnectivityManager.Error(error))) 98 | } 99 | } 100 | 101 | func session(_: WCSession, didReceiveMessage message: [String: Any]) { 102 | DispatchQueue.main.async { 103 | self.subscriber.send(.didReceiveMessage(message: NSDictionary(dictionary: message))) 104 | } 105 | } 106 | 107 | #if os(iOS) 108 | func sessionDidBecomeInactive(_: WCSession) { 109 | DispatchQueue.main.async { 110 | self.subscriber.send(.sessionDidBecomeInactive) 111 | } 112 | } 113 | 114 | func sessionDidDeactivate(_: WCSession) { 115 | DispatchQueue.main.async { 116 | self.subscriber.send(.sessionDidDeactivate) 117 | } 118 | } 119 | 120 | func sessionWatchStateDidChange(_: WCSession) { 121 | DispatchQueue.main.async { 122 | self.subscriber.send(.sessionWatchStateDidChange) 123 | } 124 | } 125 | 126 | func sessionReachabilityDidChange(_: WCSession) { 127 | DispatchQueue.main.async { 128 | self.subscriber.send(.sessionReachabilityDidChange) 129 | } 130 | } 131 | #endif 132 | 133 | func session(_: WCSession, didReceiveApplicationContext applicationContext: [String: Any]) { 134 | DispatchQueue.main.async { 135 | self.subscriber.send(.didReceiveApplicationContext(applicationContext: NSDictionary(dictionary: applicationContext))) 136 | } 137 | } 138 | } 139 | #endif 140 | -------------------------------------------------------------------------------- /Sources/ComposableWatchConnectivity/Mock.swift: -------------------------------------------------------------------------------- 1 | // Mock.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | #if canImport(WatchConnectivity) 5 | import ComposableArchitecture 6 | 7 | #if DEBUG 8 | public extension WatchConnectivityManager { 9 | static func unimplemented() -> Self { 10 | Self() 11 | } 12 | } 13 | 14 | #endif 15 | 16 | public func _unimplemented( 17 | _ function: StaticString, file: StaticString = #file, line: UInt = #line 18 | ) -> Never { 19 | fatalError( 20 | """ 21 | `\(function)` was called but is not implemented. Be sure to provide an implementation for 22 | this endpoint when creating the mock. 23 | """, 24 | file: file, 25 | line: line 26 | ) 27 | } 28 | #endif 29 | -------------------------------------------------------------------------------- /Sources/ComposableWorkout/Interface.swift: -------------------------------------------------------------------------------- 1 | // Interface.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | #if os(watchOS) 5 | import ComposableArchitecture 6 | import Foundation 7 | import HealthKit 8 | 9 | public struct WorkoutManager { 10 | public enum Action: Equatable { 11 | case workoutBuilder(workoutBuilder: HKLiveWorkoutBuilder, collectedTypes: Set) 12 | case workoutBuilderDidCollectEvent(workoutBuilder: HKLiveWorkoutBuilder) 13 | } 14 | 15 | public struct Error: Swift.Error, Equatable { 16 | public let error: NSError? 17 | 18 | public init(_ error: Swift.Error?) { 19 | self.error = error as NSError? 20 | } 21 | } 22 | 23 | // MARK: - Variables 24 | 25 | var create: (AnyHashable, HKWorkoutConfiguration) -> Effect = { _, _ in _unimplemented("create") } 26 | 27 | var destroy: (AnyHashable) -> Effect = { _ in _unimplemented("destroy") } 28 | 29 | var startActivity: (AnyHashable, Date) -> Effect = { _, _ in _unimplemented("startActivity") } 30 | 31 | var pauseSession: (AnyHashable) -> Effect = { _ in _unimplemented("pauseSession") } 32 | 33 | var resumeSession: (AnyHashable) -> Effect = { _ in _unimplemented("resumeSession") } 34 | 35 | var discardWorkout: (AnyHashable, Date) -> Effect = { _, _ in _unimplemented("discardWorkout") } 36 | 37 | var finishWorkout: (AnyHashable, Date) -> Effect = { _, _ in _unimplemented("finishWorkout") } 38 | 39 | // MARK: - Functions 40 | 41 | public func create(id: AnyHashable, workoutConfiguration: HKWorkoutConfiguration) -> Effect { 42 | create(id, workoutConfiguration) 43 | } 44 | 45 | public func destroy(id: AnyHashable) -> Effect { 46 | destroy(id) 47 | } 48 | 49 | public func startActivity(id: AnyHashable, date: Date) -> Effect { 50 | startActivity(id, date) 51 | } 52 | 53 | public func pauseSession(id: AnyHashable) -> Effect { 54 | pauseSession(id) 55 | } 56 | 57 | public func resumeSession(id: AnyHashable) -> Effect { 58 | resumeSession(id) 59 | } 60 | 61 | public func discardWorkout(id: AnyHashable, date: Date) -> Effect { 62 | discardWorkout(id, date) 63 | } 64 | 65 | public func finishWorkout(id: AnyHashable, date: Date) -> Effect { 66 | finishWorkout(id, date) 67 | } 68 | } 69 | #endif 70 | -------------------------------------------------------------------------------- /Sources/ComposableWorkout/Live.swift: -------------------------------------------------------------------------------- 1 | // Live.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | #if os(watchOS) 5 | import Combine 6 | import ComposableArchitecture 7 | import Foundation 8 | import HealthKit 9 | 10 | public extension WorkoutManager { 11 | static let live: WorkoutManager = { () -> WorkoutManager in 12 | 13 | var manager = WorkoutManager() 14 | 15 | manager.create = { id, workoutConfiguration in 16 | Effect.run { subscriber in 17 | let healthStore = HKHealthStore() 18 | 19 | var workoutSessionDelegate = WorkoutSessionDelegate(subscriber) 20 | var liveWorkoutBuilderDelegate = LiveWorkoutBuilderDelegate(subscriber) 21 | 22 | var workoutSession: HKWorkoutSession? 23 | do { 24 | workoutSession = try HKWorkoutSession(healthStore: healthStore, configuration: workoutConfiguration) 25 | } catch { 26 | return AnyCancellable { 27 | dependencies[id] = nil 28 | } 29 | } 30 | 31 | var workoutBuilder = workoutSession?.associatedWorkoutBuilder() 32 | workoutBuilder?.dataSource = HKLiveWorkoutDataSource(healthStore: healthStore, workoutConfiguration: workoutConfiguration) 33 | 34 | workoutSession?.delegate = workoutSessionDelegate 35 | workoutBuilder?.delegate = liveWorkoutBuilderDelegate 36 | 37 | dependencies[id] = Dependencies( 38 | healthStore: healthStore, 39 | workoutSession: workoutSession, 40 | workoutBuilder: workoutBuilder, 41 | liveWorkoutBuilderDelegate: liveWorkoutBuilderDelegate, 42 | workoutSessionDelegate: workoutSessionDelegate, 43 | subscriber: subscriber 44 | ) 45 | 46 | return AnyCancellable { 47 | dependencies[id] = nil 48 | } 49 | } 50 | } 51 | 52 | manager.startActivity = { id, date in 53 | .fireAndForget { 54 | dependencies[id]?.workoutBuilder?.beginCollection(withStart: date) { _, _ in } 55 | dependencies[id]?.workoutSession?.prepare() 56 | dependencies[id]?.workoutSession?.startActivity(with: date) 57 | } 58 | } 59 | 60 | manager.pauseSession = { id in 61 | .fireAndForget { 62 | dependencies[id]?.workoutSession?.pause() 63 | } 64 | } 65 | 66 | manager.resumeSession = { id in 67 | .fireAndForget { 68 | dependencies[id]?.workoutSession?.resume() 69 | } 70 | } 71 | 72 | manager.discardWorkout = { id, _ in 73 | .fireAndForget { 74 | dependencies[id]?.workoutSession?.end() 75 | dependencies[id]?.workoutBuilder?.discardWorkout() 76 | } 77 | } 78 | 79 | manager.finishWorkout = { id, date in 80 | .fireAndForget { 81 | dependencies[id]?.workoutSession?.end() 82 | dependencies[id]?.workoutBuilder?.endCollection(withEnd: date) { _, _ in 83 | dependencies[id]?.workoutBuilder?.finishWorkout { _, _ in } 84 | } 85 | } 86 | } 87 | 88 | manager.destroy = { id in 89 | .fireAndForget { 90 | dependencies[id]?.subscriber.send(completion: .finished) 91 | dependencies[id] = nil 92 | } 93 | } 94 | 95 | return manager 96 | }() 97 | } 98 | 99 | private struct Dependencies { 100 | let healthStore: HKHealthStore 101 | let workoutSession: HKWorkoutSession? 102 | let workoutBuilder: HKWorkoutBuilder? 103 | let liveWorkoutBuilderDelegate: LiveWorkoutBuilderDelegate 104 | let workoutSessionDelegate: WorkoutSessionDelegate 105 | let subscriber: Effect.Subscriber 106 | } 107 | 108 | private var dependencies: [AnyHashable: Dependencies] = [:] 109 | 110 | // MARK: - HKLiveWorkoutBuilderDelegate 111 | 112 | private class LiveWorkoutBuilderDelegate: NSObject, HKLiveWorkoutBuilderDelegate { 113 | let subscriber: Effect.Subscriber 114 | 115 | init(_ subscriber: Effect.Subscriber) { 116 | self.subscriber = subscriber 117 | } 118 | 119 | func workoutBuilderDidCollectEvent(_ workoutBuilder: HKLiveWorkoutBuilder) { 120 | subscriber.send(.workoutBuilderDidCollectEvent(workoutBuilder: workoutBuilder)) 121 | } 122 | 123 | func workoutBuilder(_ workoutBuilder: HKLiveWorkoutBuilder, didCollectDataOf collectedTypes: Set) { 124 | DispatchQueue.main.async { 125 | self.subscriber.send(.workoutBuilder(workoutBuilder: workoutBuilder, collectedTypes: collectedTypes)) 126 | } 127 | } 128 | } 129 | 130 | // MARK: - HKWorkoutSessionDelegate 131 | 132 | private class WorkoutSessionDelegate: NSObject, HKWorkoutSessionDelegate { 133 | let subscriber: Effect.Subscriber 134 | 135 | init(_ subscriber: Effect.Subscriber) { 136 | self.subscriber = subscriber 137 | } 138 | 139 | func workoutSession(_: HKWorkoutSession, didChangeTo _: HKWorkoutSessionState, from _: HKWorkoutSessionState, date _: Date) {} 140 | 141 | func workoutSession(_: HKWorkoutSession, didFailWithError _: Error) {} 142 | } 143 | 144 | #endif 145 | -------------------------------------------------------------------------------- /Sources/ComposableWorkout/Mock.swift: -------------------------------------------------------------------------------- 1 | // Mock.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | #if os(watchOS) 5 | import ComposableArchitecture 6 | 7 | #if DEBUG 8 | public extension WorkoutManager { 9 | static func unimplemented() -> Self { 10 | Self() 11 | } 12 | } 13 | 14 | #endif 15 | 16 | public func _unimplemented( 17 | _ function: StaticString, file: StaticString = #file, line: UInt = #line 18 | ) -> Never { 19 | fatalError( 20 | """ 21 | `\(function)` was called but is not implemented. Be sure to provide an implementation for 22 | this endpoint when creating the mock. 23 | """, 24 | file: file, 25 | line: line 26 | ) 27 | } 28 | #endif 29 | -------------------------------------------------------------------------------- /Tests/ComposableAudioPlayerTests/ComposableAudioPlayerTests.swift: -------------------------------------------------------------------------------- 1 | // ComposableAudioPlayerTests.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | import XCTest 5 | @testable import ComposableAudioPlayer 6 | 7 | final class ComposableAudioPlayerTests: XCTestCase { 8 | func testExample() { 9 | XCTAssertTrue(true) 10 | } 11 | 12 | static var allTests = [ 13 | ("testExample", testExample), 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /Tests/ComposableAudioRecorderTests/ComposableAudioRecorderTests.swift: -------------------------------------------------------------------------------- 1 | // ComposableAudioRecorderTests.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | import XCTest 5 | @testable import ComposableAudioRecorder 6 | 7 | final class ComposableAudioRecorderTests: XCTestCase { 8 | func testExample() { 9 | XCTAssertTrue(true) 10 | } 11 | 12 | static var allTests = [ 13 | ("testExample", testExample), 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /Tests/ComposableBluetoothCentralManagerTests/ComposableBluetoothCentralManagerTests.swift: -------------------------------------------------------------------------------- 1 | // ComposableBluetoothCentralManagerTests.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | import XCTest 5 | @testable import ComposableBluetoothCentralManager 6 | 7 | final class ComposableBluetoothCentralManagerTests: XCTestCase { 8 | func testExample() { 9 | XCTAssertTrue(true) 10 | } 11 | 12 | static var allTests = [ 13 | ("testExample", testExample), 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /Tests/ComposableBluetoothPeripheralManagerTests/ComposableBluetoothPeripheralManagerTests.swift: -------------------------------------------------------------------------------- 1 | // ComposableBluetoothPeripheralManagerTests.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | import XCTest 5 | @testable import ComposableBluetoothPeripheralManager 6 | 7 | final class ComposableBluetoothPeripheralManagerTests: XCTestCase { 8 | func testExample() { 9 | XCTAssertTrue(true) 10 | } 11 | 12 | static var allTests = [ 13 | ("testExample", testExample), 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /Tests/ComposableCoreLocationTests/ComposableCoreLocationTests.swift: -------------------------------------------------------------------------------- 1 | // ComposableCoreLocationTests.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | import ComposableCoreLocation 5 | import XCTest 6 | 7 | class ComposableCoreLocationTests: XCTestCase { 8 | func testMockHasDefaultsForAllEndpoints() { 9 | _ = LocationManager.unimplemented() 10 | } 11 | 12 | func testLocationEncodeDecode() { 13 | let value: Location 14 | 15 | #if compiler(>=5.2) 16 | if #available(iOS 13.4, macCatalyst 13.4, macOS 10.15.4, tvOS 13.4, watchOS 6.2, *) { 17 | value = Location( 18 | altitude: 50, 19 | coordinate: CLLocationCoordinate2D(latitude: 10, longitude: 20), 20 | course: 9, 21 | courseAccuracy: 1, 22 | horizontalAccuracy: 3, 23 | speed: 5, 24 | speedAccuracy: 2, 25 | timestamp: Date(timeIntervalSince1970: 0), 26 | verticalAccuracy: 6 27 | ) 28 | } else { 29 | value = Location( 30 | altitude: 50, 31 | coordinate: CLLocationCoordinate2D(latitude: 10, longitude: 20), 32 | course: 9, 33 | horizontalAccuracy: 3, 34 | speed: 5, 35 | timestamp: Date(timeIntervalSince1970: 0), 36 | verticalAccuracy: 6 37 | ) 38 | } 39 | #else 40 | value = Location( 41 | altitude: 50, 42 | coordinate: CLLocationCoordinate2D(latitude: 10, longitude: 20), 43 | course: 9, 44 | horizontalAccuracy: 3, 45 | speed: 5, 46 | timestamp: Date(timeIntervalSince1970: 0), 47 | verticalAccuracy: 6 48 | ) 49 | #endif 50 | 51 | let data = try? JSONEncoder().encode(value) 52 | let decoded = try? JSONDecoder().decode(Location.self, from: data ?? Data()) 53 | 54 | XCTAssertEqual(value, decoded) 55 | } 56 | 57 | func testLocationEquatable() { 58 | let a = Location( 59 | altitude: 1, 60 | coordinate: CLLocationCoordinate2D(latitude: 1, longitude: 1), 61 | course: 1, 62 | horizontalAccuracy: 1, 63 | speed: 1, 64 | timestamp: Date(timeIntervalSince1970: 0), 65 | verticalAccuracy: 1 66 | ) 67 | 68 | let b = Location( 69 | altitude: 2, 70 | coordinate: CLLocationCoordinate2D(latitude: 2, longitude: 2), 71 | course: 2, 72 | horizontalAccuracy: 2, 73 | speed: 2, 74 | timestamp: Date(timeIntervalSince1970: 1), 75 | verticalAccuracy: 2 76 | ) 77 | 78 | XCTAssertTrue(a == a) 79 | XCTAssertFalse(a == b) 80 | } 81 | 82 | #if compiler(>=5.2) 83 | func testLocationEquatable_5_2() { 84 | if #available(iOS 13.4, macCatalyst 13.4, macOS 10.15.4, tvOS 13.4, watchOS 6.2, *) { 85 | let a = Location( 86 | altitude: 1, 87 | coordinate: CLLocationCoordinate2D(latitude: 1, longitude: 1), 88 | course: 1, 89 | courseAccuracy: 1, 90 | horizontalAccuracy: 1, 91 | speed: 1, 92 | speedAccuracy: 1, 93 | timestamp: Date(timeIntervalSince1970: 0), 94 | verticalAccuracy: 1 95 | ) 96 | 97 | let b = Location( 98 | altitude: 1, 99 | coordinate: CLLocationCoordinate2D(latitude: 1, longitude: 1), 100 | course: 1, 101 | courseAccuracy: 1, 102 | horizontalAccuracy: 1, 103 | speed: 1, 104 | speedAccuracy: 2, 105 | timestamp: Date(timeIntervalSince1970: 0), 106 | verticalAccuracy: 1 107 | ) 108 | 109 | let c = Location( 110 | altitude: 1, 111 | coordinate: CLLocationCoordinate2D(latitude: 1, longitude: 1), 112 | course: 1, 113 | courseAccuracy: 2, 114 | horizontalAccuracy: 1, 115 | speed: 1, 116 | speedAccuracy: 1, 117 | timestamp: Date(timeIntervalSince1970: 0), 118 | verticalAccuracy: 1 119 | ) 120 | 121 | XCTAssertTrue(a == a) 122 | XCTAssertFalse(a == b) 123 | XCTAssertFalse(a == c) 124 | } 125 | } 126 | #endif 127 | } 128 | -------------------------------------------------------------------------------- /Tests/ComposableCoreMotionTests/ComposableCoreMotionTests.swift: -------------------------------------------------------------------------------- 1 | // ComposableCoreMotionTests.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | #if canImport(CoreMotion) 5 | import XCTest 6 | 7 | @testable import ComposableCoreMotion 8 | 9 | class ComposableCoreMotionTests: XCTestCase { 10 | func testQuaternionInverse() { 11 | let q = CMQuaternion(x: 1, y: 2, z: 3, w: 4) 12 | let inv = q.inverse 13 | let result = q.multiplied(by: inv) 14 | 15 | XCTAssertEqual(result.x, 0) 16 | XCTAssertEqual(result.y, 0) 17 | XCTAssertEqual(result.z, 0) 18 | XCTAssertEqual(result.w, 1) 19 | } 20 | 21 | func testQuaternionMultiplication() { 22 | let q1 = CMQuaternion(x: 1, y: 2, z: 3, w: 4) 23 | let q2 = CMQuaternion(x: -4, y: 3, z: -2, w: 1) 24 | let result = q1.multiplied(by: q2) 25 | 26 | XCTAssertEqual(result.x, -28) 27 | XCTAssertEqual(result.y, 4) 28 | XCTAssertEqual(result.z, 6) 29 | XCTAssertEqual(result.w, 8) 30 | } 31 | 32 | func testRollPitchYaw() { 33 | let q1 = Attitude(quaternion: .init(x: 1, y: 0, z: 0, w: 0)) 34 | let q2 = Attitude(quaternion: .init(x: 0, y: 1, z: 0, w: 0)) 35 | let q3 = Attitude(quaternion: .init(x: 0, y: 0, z: 1, w: 0)) 36 | let q4 = Attitude(quaternion: .init(x: 0, y: 0, z: 0, w: 1)) 37 | 38 | XCTAssertEqual(q1.roll, Double.pi) 39 | XCTAssertEqual(q1.pitch, 0) 40 | XCTAssertEqual(q1.yaw, 0) 41 | 42 | XCTAssertEqual(q2.roll, Double.pi) 43 | XCTAssertEqual(q2.pitch, 0) 44 | XCTAssertEqual(q2.yaw, Double.pi) 45 | 46 | XCTAssertEqual(q3.roll, 0) 47 | XCTAssertEqual(q3.pitch, 0) 48 | XCTAssertEqual(q3.yaw, Double.pi) 49 | 50 | XCTAssertEqual(q4.roll, 0) 51 | XCTAssertEqual(q4.pitch, 0) 52 | XCTAssertEqual(q4.yaw, 0) 53 | } 54 | } 55 | #endif 56 | -------------------------------------------------------------------------------- /Tests/ComposableFastTests/ComposableFastTests.swift: -------------------------------------------------------------------------------- 1 | // ComposableFastTests.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | import XCTest 5 | @testable import ComposableFast 6 | 7 | final class ComposableFastTests: XCTestCase { 8 | func testExample() { 9 | XCTAssertTrue(true) 10 | } 11 | 12 | static var allTests = [ 13 | ("testExample", testExample), 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /Tests/ComposablePlayerTests/ComposableAudioPlayerTests.swift: -------------------------------------------------------------------------------- 1 | // ComposableAudioPlayerTests.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | import XCTest 5 | @testable import ComposablePlayer 6 | 7 | final class ComposablePlayerTests: XCTestCase { 8 | func testExample() { 9 | XCTAssertTrue(true) 10 | } 11 | 12 | static var allTests = [ 13 | ("testExample", testExample), 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /Tests/ComposableSpeechRecognizerTests/ComposableSpeechRecognizerTests.swift: -------------------------------------------------------------------------------- 1 | // ComposableSpeechRecognizerTests.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | import XCTest 5 | @testable import ComposableSpeechRecognizer 6 | 7 | final class ComposableSpeechRecognizerTests: XCTestCase { 8 | func testExample() { 9 | XCTAssertTrue(true) 10 | } 11 | 12 | static var allTests = [ 13 | ("testExample", testExample), 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /Tests/ComposableSpeechSynthesizerTests/ComposableSpeechSynthesizerTests.swift: -------------------------------------------------------------------------------- 1 | // ComposableSpeechSynthesizerTests.swift 2 | // Copyright (c) 2021 Joe Blau 3 | 4 | import XCTest 5 | @testable import ComposableSpeechSynthesizer 6 | 7 | final class ComposableSpeechSynthesizerTests: XCTestCase { 8 | func testExample() { 9 | XCTAssertTrue(true) 10 | } 11 | 12 | static var allTests = [ 13 | ("testExample", testExample), 14 | ] 15 | } 16 | --------------------------------------------------------------------------------