From 363839a87ee57bf2b73285921243dca982397eef Mon Sep 17 00:00:00 2001 From: Roberto Casula Date: Mon, 25 Nov 2024 06:51:01 +0100 Subject: [PATCH] Rename library to CoreLocationClient --- .../xcschemes/ComposableCoreLocation.xcscheme | 77 --- .../ComposableCoreLocation_watchOS.xcscheme | 67 --- Examples/LocationManager/Common/AppCore.swift | 2 +- .../Common/LocalSearchClient/Client.swift | 18 +- .../Common/LocalSearchClient/Live.swift | 3 - .../CommonTests/CommonTests.swift | 2 +- .../Desktop/LocationManagerView.swift | 2 +- .../LocationManager.xcodeproj/project.pbxproj | 32 +- .../xcshareddata/swiftpm/Package.resolved | 34 +- .../Mobile/LocationManagerView.swift | 16 +- Package.resolved | 32 +- Package.swift | 35 +- Package@swift-6.0.swift | 47 ++ README.md | 2 +- Sources/ComposableCoreLocation/Failing.swift | 74 --- .../ComposableCoreLocation/Interface.swift | 437 ----------------- .../Dependencies.swift | 4 +- Sources/CoreLocationClient/Interface.swift | 439 ++++++++++++++++++ .../Internal/Deprecations.swift | 0 .../Internal/Exports.swift | 0 .../Live.swift | 8 +- .../Models/AccuracyAuthorization.swift | 0 .../Models/Beacon.swift | 0 .../Models/BeaconConstraint.swift | 0 .../Models/Heading.swift | 0 .../Models/Location.swift | 0 .../Models/Region.swift | 0 .../Models/Visit.swift | 0 .../ComposableCoreLocationTests.swift | 2 +- 29 files changed, 588 insertions(+), 745 deletions(-) delete mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/ComposableCoreLocation.xcscheme delete mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/ComposableCoreLocation_watchOS.xcscheme create mode 100644 Package@swift-6.0.swift delete mode 100644 Sources/ComposableCoreLocation/Failing.swift delete mode 100644 Sources/ComposableCoreLocation/Interface.swift rename Sources/{ComposableCoreLocation => CoreLocationClient}/Dependencies.swift (73%) create mode 100644 Sources/CoreLocationClient/Interface.swift rename Sources/{ComposableCoreLocation => CoreLocationClient}/Internal/Deprecations.swift (100%) rename Sources/{ComposableCoreLocation => CoreLocationClient}/Internal/Exports.swift (100%) rename Sources/{ComposableCoreLocation => CoreLocationClient}/Live.swift (97%) rename Sources/{ComposableCoreLocation => CoreLocationClient}/Models/AccuracyAuthorization.swift (100%) rename Sources/{ComposableCoreLocation => CoreLocationClient}/Models/Beacon.swift (100%) rename Sources/{ComposableCoreLocation => CoreLocationClient}/Models/BeaconConstraint.swift (100%) rename Sources/{ComposableCoreLocation => CoreLocationClient}/Models/Heading.swift (100%) rename Sources/{ComposableCoreLocation => CoreLocationClient}/Models/Location.swift (100%) rename Sources/{ComposableCoreLocation => CoreLocationClient}/Models/Region.swift (100%) rename Sources/{ComposableCoreLocation => CoreLocationClient}/Models/Visit.swift (100%) rename Tests/{ComposableCoreLocationTests => CoreLocationClientTests}/ComposableCoreLocationTests.swift (99%) diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/ComposableCoreLocation.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/ComposableCoreLocation.xcscheme deleted file mode 100644 index 97ba321..0000000 --- a/.swiftpm/xcode/xcshareddata/xcschemes/ComposableCoreLocation.xcscheme +++ /dev/null @@ -1,77 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/ComposableCoreLocation_watchOS.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/ComposableCoreLocation_watchOS.xcscheme deleted file mode 100644 index f8c322e..0000000 --- a/.swiftpm/xcode/xcshareddata/xcschemes/ComposableCoreLocation_watchOS.xcscheme +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Examples/LocationManager/Common/AppCore.swift b/Examples/LocationManager/Common/AppCore.swift index 0e4356a..84e424b 100644 --- a/Examples/LocationManager/Common/AppCore.swift +++ b/Examples/LocationManager/Common/AppCore.swift @@ -1,5 +1,5 @@ import ComposableArchitecture -import ComposableCoreLocation +import CoreLocationClient import MapKit public struct PointOfInterest: Equatable, Hashable { diff --git a/Examples/LocationManager/Common/LocalSearchClient/Client.swift b/Examples/LocationManager/Common/LocalSearchClient/Client.swift index c37868b..f281ddc 100644 --- a/Examples/LocationManager/Common/LocalSearchClient/Client.swift +++ b/Examples/LocationManager/Common/LocalSearchClient/Client.swift @@ -2,21 +2,21 @@ import MapKit import Dependencies extension DependencyValues { - public var localSearchClient: LocalSearchClient { - get { self[LocalSearchClient.self] } - set { self[LocalSearchClient.self] = newValue } - } + public var localSearchClient: LocalSearchClient { + get { self[LocalSearchClient.self] } + set { self[LocalSearchClient.self] = newValue } + } } extension LocalSearchClient: TestDependencyKey { - public static let previewValue = Self.noop - public static let testValue = Self.failing + public static let previewValue = Self.noop + public static let testValue = Self.noop } extension LocalSearchClient { - public static let noop = Self( - search: { _ in try await Task.never() } - ) + public static let noop = Self( + search: { _ in try await Task.never() } + ) } public struct LocalSearchClient { diff --git a/Examples/LocationManager/Common/LocalSearchClient/Live.swift b/Examples/LocationManager/Common/LocalSearchClient/Live.swift index 79780d6..392e677 100644 --- a/Examples/LocationManager/Common/LocalSearchClient/Live.swift +++ b/Examples/LocationManager/Common/LocalSearchClient/Live.swift @@ -1,9 +1,6 @@ -import Combine import ComposableArchitecture import MapKit - - extension LocalSearchClient: DependencyKey { public static let liveValue = Self( search: { request in diff --git a/Examples/LocationManager/CommonTests/CommonTests.swift b/Examples/LocationManager/CommonTests/CommonTests.swift index 286215b..b81e0fa 100644 --- a/Examples/LocationManager/CommonTests/CommonTests.swift +++ b/Examples/LocationManager/CommonTests/CommonTests.swift @@ -1,6 +1,6 @@ import Combine import ComposableArchitecture -import ComposableCoreLocation +import CoreLocationClient import CoreLocation import MapKit import XCTest diff --git a/Examples/LocationManager/Desktop/LocationManagerView.swift b/Examples/LocationManager/Desktop/LocationManagerView.swift index 7f1446f..0691ab7 100644 --- a/Examples/LocationManager/Desktop/LocationManagerView.swift +++ b/Examples/LocationManager/Desktop/LocationManagerView.swift @@ -1,6 +1,6 @@ import Combine import ComposableArchitecture -import ComposableCoreLocation +import CoreLocationClient import MapKit import SwiftUI diff --git a/Examples/LocationManager/LocationManager.xcodeproj/project.pbxproj b/Examples/LocationManager/LocationManager.xcodeproj/project.pbxproj index 8aea16a..8f00d33 100644 --- a/Examples/LocationManager/LocationManager.xcodeproj/project.pbxproj +++ b/Examples/LocationManager/LocationManager.xcodeproj/project.pbxproj @@ -7,22 +7,20 @@ objects = { /* Begin PBXBuildFile section */ + 6A102C722CF36059006E5DB6 /* CoreLocationClient in Frameworks */ = {isa = PBXBuildFile; productRef = 6A102C712CF36059006E5DB6 /* CoreLocationClient */; }; + 6A102C742CF36066006E5DB6 /* CoreLocationClient in Frameworks */ = {isa = PBXBuildFile; productRef = 6A102C732CF36066006E5DB6 /* CoreLocationClient */; }; CA17CC0F24720BBA00BDDF11 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA17CC0E24720BBA00BDDF11 /* SceneDelegate.swift */; }; CA17CC1124720BBA00BDDF11 /* LocationManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA17CC1024720BBA00BDDF11 /* LocationManagerView.swift */; }; CA17CC1324720BBB00BDDF11 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CA17CC1224720BBB00BDDF11 /* Assets.xcassets */; }; CA17CC3924720BEB00BDDF11 /* ComposableArchitecture in Frameworks */ = {isa = PBXBuildFile; productRef = CA17CC3824720BEB00BDDF11 /* ComposableArchitecture */; }; - CA17CCB62474582000BDDF11 /* ComposableCoreLocation in Frameworks */ = {isa = PBXBuildFile; productRef = CA17CCB52474582000BDDF11 /* ComposableCoreLocation */; }; CA17CCC024745B1000BDDF11 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA17CCBF24745B1000BDDF11 /* AppDelegate.swift */; }; CA17CCC224745B1000BDDF11 /* LocationManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA17CCC124745B1000BDDF11 /* LocationManagerView.swift */; }; CA17CCC424745B1000BDDF11 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CA17CCC324745B1000BDDF11 /* Assets.xcassets */; }; CA17CCE324745B1A00BDDF11 /* ComposableArchitecture in Frameworks */ = {isa = PBXBuildFile; productRef = CA17CCE224745B1A00BDDF11 /* ComposableArchitecture */; }; - CA17CCE524745B1A00BDDF11 /* ComposableCoreLocation in Frameworks */ = {isa = PBXBuildFile; productRef = CA17CCE424745B1A00BDDF11 /* ComposableCoreLocation */; }; CA74947D2474A4F100B13019 /* MapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA74946B2474A4F100B13019 /* MapView.swift */; }; CA74947E2474A4F100B13019 /* MapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA74946B2474A4F100B13019 /* MapView.swift */; }; CA74947F2474A4F100B13019 /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA74946C2474A4F100B13019 /* Helpers.swift */; }; CA7494802474A4F100B13019 /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA74946C2474A4F100B13019 /* Helpers.swift */; }; - CA7494812474A4F100B13019 /* Failing.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA74946E2474A4F100B13019 /* Failing.swift */; }; - CA7494822474A4F100B13019 /* Failing.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA74946E2474A4F100B13019 /* Failing.swift */; }; CA7494832474A4F100B13019 /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA74946F2474A4F100B13019 /* Client.swift */; }; CA7494842474A4F100B13019 /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA74946F2474A4F100B13019 /* Client.swift */; }; CA7494852474A4F100B13019 /* Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA7494702474A4F100B13019 /* Models.swift */; }; @@ -67,7 +65,6 @@ CA17CCCC24745B1000BDDF11 /* LocationManagerDesktop.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = LocationManagerDesktop.entitlements; sourceTree = ""; }; CA74946B2474A4F100B13019 /* MapView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MapView.swift; sourceTree = ""; }; CA74946C2474A4F100B13019 /* Helpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = ""; }; - CA74946E2474A4F100B13019 /* Failing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Failing.swift; sourceTree = ""; }; CA74946F2474A4F100B13019 /* Client.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Client.swift; sourceTree = ""; }; CA7494702474A4F100B13019 /* Models.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Models.swift; sourceTree = ""; }; CA7494712474A4F100B13019 /* Live.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Live.swift; sourceTree = ""; }; @@ -85,8 +82,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - CA17CCB62474582000BDDF11 /* ComposableCoreLocation in Frameworks */, CA17CC3924720BEB00BDDF11 /* ComposableArchitecture in Frameworks */, + 6A102C722CF36059006E5DB6 /* CoreLocationClient in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -94,8 +91,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - CA17CCE524745B1A00BDDF11 /* ComposableCoreLocation in Frameworks */, CA17CCE324745B1A00BDDF11 /* ComposableArchitecture in Frameworks */, + 6A102C742CF36066006E5DB6 /* CoreLocationClient in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -185,7 +182,6 @@ CA74946D2474A4F100B13019 /* LocalSearchClient */ = { isa = PBXGroup; children = ( - CA74946E2474A4F100B13019 /* Failing.swift */, CA74946F2474A4F100B13019 /* Client.swift */, CA7494702474A4F100B13019 /* Models.swift */, CA7494712474A4F100B13019 /* Live.swift */, @@ -220,7 +216,7 @@ name = LocationManagerMobile; packageProductDependencies = ( CA17CC3824720BEB00BDDF11 /* ComposableArchitecture */, - CA17CCB52474582000BDDF11 /* ComposableCoreLocation */, + 6A102C712CF36059006E5DB6 /* CoreLocationClient */, ); productName = LocationManager; productReference = CA17CC0924720BBA00BDDF11 /* LocationManagerMobile.app */; @@ -241,7 +237,7 @@ name = LocationManagerDesktop; packageProductDependencies = ( CA17CCE224745B1A00BDDF11 /* ComposableArchitecture */, - CA17CCE424745B1A00BDDF11 /* ComposableCoreLocation */, + 6A102C732CF36066006E5DB6 /* CoreLocationClient */, ); productName = LocationManagerDesktop; productReference = CA17CCBD24745B1000BDDF11 /* LocationManagerDesktop.app */; @@ -374,7 +370,6 @@ files = ( CA7494892474A4F100B13019 /* AppCore.swift in Sources */, CA17CC0F24720BBA00BDDF11 /* SceneDelegate.swift in Sources */, - CA7494812474A4F100B13019 /* Failing.swift in Sources */, CA7494832474A4F100B13019 /* Client.swift in Sources */, CA74947D2474A4F100B13019 /* MapView.swift in Sources */, CA17CC1124720BBA00BDDF11 /* LocationManagerView.swift in Sources */, @@ -390,7 +385,6 @@ files = ( CA74948A2474A4F100B13019 /* AppCore.swift in Sources */, CA17CCC224745B1000BDDF11 /* LocationManagerView.swift in Sources */, - CA7494822474A4F100B13019 /* Failing.swift in Sources */, CA7494842474A4F100B13019 /* Client.swift in Sources */, CA74947E2474A4F100B13019 /* MapView.swift in Sources */, CA17CCC024745B1000BDDF11 /* AppDelegate.swift in Sources */, @@ -785,21 +779,21 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - CA17CC3824720BEB00BDDF11 /* ComposableArchitecture */ = { + 6A102C712CF36059006E5DB6 /* CoreLocationClient */ = { isa = XCSwiftPackageProductDependency; - productName = ComposableArchitecture; + productName = CoreLocationClient; }; - CA17CCB52474582000BDDF11 /* ComposableCoreLocation */ = { + 6A102C732CF36066006E5DB6 /* CoreLocationClient */ = { isa = XCSwiftPackageProductDependency; - productName = ComposableCoreLocation; + productName = CoreLocationClient; }; - CA17CCE224745B1A00BDDF11 /* ComposableArchitecture */ = { + CA17CC3824720BEB00BDDF11 /* ComposableArchitecture */ = { isa = XCSwiftPackageProductDependency; productName = ComposableArchitecture; }; - CA17CCE424745B1A00BDDF11 /* ComposableCoreLocation */ = { + CA17CCE224745B1A00BDDF11 /* ComposableArchitecture */ = { isa = XCSwiftPackageProductDependency; - productName = ComposableCoreLocation; + productName = ComposableArchitecture; }; /* End XCSwiftPackageProductDependency section */ }; diff --git a/Examples/LocationManager/LocationManager.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Examples/LocationManager/LocationManager.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index bb3407f..ba91c61 100644 --- a/Examples/LocationManager/LocationManager.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Examples/LocationManager/LocationManager.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,12 +1,13 @@ { + "originHash" : "958a4d0fa159e09bc62a61c84c233f6e7100c3292b86267844720ec627c0a056", "pins" : [ { "identity" : "combine-schedulers", "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/combine-schedulers", "state" : { - "revision" : "9dc9cbe4bc45c65164fa653a563d8d8db61b09bb", - "version" : "1.0.0" + "revision" : "9fa31f4403da54855f1e2aeaeff478f4f0e40b13", + "version" : "1.0.2" } }, { @@ -23,8 +24,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-clocks", "state" : { - "revision" : "d1fd837326aa719bee979bdde1f53cd5797443eb", - "version" : "1.0.0" + "revision" : "b9b24b69e2adda099a1fa381cda1eeec272d5b53", + "version" : "1.0.5" } }, { @@ -39,7 +40,7 @@ { "identity" : "swift-composable-architecture", "kind" : "remoteSourceControl", - "location" : "git@github.com:pointfreeco/swift-composable-architecture.git", + "location" : "https://github.com/pointfreeco/swift-composable-architecture", "state" : { "revision" : "a7c1f799b55ecb418f85094b142565834f7ee7c7", "version" : "1.2.0" @@ -50,8 +51,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-concurrency-extras", "state" : { - "revision" : "ea631ce892687f5432a833312292b80db238186a", - "version" : "1.0.0" + "revision" : "163409ef7dae9d960b87f34b51587b6609a76c1f", + "version" : "1.3.0" } }, { @@ -68,8 +69,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-dependencies", "state" : { - "revision" : "4e1eb6e28afe723286d8cc60611237ffbddba7c5", - "version" : "1.0.0" + "revision" : "e2b06090b0b7738fcd8c762131e705d711fc7e8e", + "version" : "1.6.0" } }, { @@ -81,6 +82,15 @@ "version" : "1.0.0" } }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-syntax", + "state" : { + "revision" : "0687f71944021d616d34d922343dcef086855920", + "version" : "600.0.1" + } + }, { "identity" : "swiftui-navigation", "kind" : "remoteSourceControl", @@ -95,10 +105,10 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", "state" : { - "revision" : "23cbf2294e350076ea4dbd7d5d047c1e76b03631", - "version" : "1.0.2" + "revision" : "a3f634d1a409c7979cabc0a71b3f26ffa9fc8af1", + "version" : "1.4.3" } } ], - "version" : 2 + "version" : 3 } diff --git a/Examples/LocationManager/Mobile/LocationManagerView.swift b/Examples/LocationManager/Mobile/LocationManagerView.swift index 277bd96..d5b44eb 100644 --- a/Examples/LocationManager/Mobile/LocationManagerView.swift +++ b/Examples/LocationManager/Mobile/LocationManagerView.swift @@ -1,6 +1,6 @@ import Combine import ComposableArchitecture -import ComposableCoreLocation +import CoreLocationClient import MapKit import SwiftUI @@ -119,16 +119,16 @@ extension LocationManager { static func mock() -> Self { actor MockStore { - let locationManagerSubject: CurrentValueSubject + let locationManagerSubject: LockIsolated> var currentAuthorizationStatus: CLAuthorizationStatus { didSet { - locationManagerSubject.send(.didChangeAuthorization(currentAuthorizationStatus)) + locationManagerSubject.value.send(.didChangeAuthorization(currentAuthorizationStatus)) } } - var currentLocation: ComposableCoreLocation.Location? { + var currentLocation: CoreLocationClient.Location? { didSet { - locationManagerSubject.send( + locationManagerSubject.value.send( .didUpdateLocations(currentLocation.map { [$0] } ?? []) ) } @@ -136,14 +136,14 @@ extension LocationManager { init(authorization: CLAuthorizationStatus) { self.currentAuthorizationStatus = authorization - self.locationManagerSubject = .init(.didChangeAuthorization(currentAuthorizationStatus)) + self.locationManagerSubject = .init(.init(.didChangeAuthorization(authorization))) } func update(authorization: CLAuthorizationStatus) { self.currentAuthorizationStatus = authorization } - func update(location: ComposableCoreLocation.Location) { + func update(location: CoreLocationClient.Location) { self.currentLocation = location } } @@ -159,7 +159,7 @@ extension LocationManager { manager.delegate = { AsyncStream { continuation in - let cancellable = store.locationManagerSubject.sink { action in + let cancellable = store.locationManagerSubject.value.sink { action in continuation.yield(action) } continuation.onTermination = { _ in diff --git a/Package.resolved b/Package.resolved index e2cfef1..0027a6c 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,12 +1,13 @@ { + "originHash" : "d8b7a276923e825c0118b9f2208446ff870e01ff3b4c61026967140f3677f7e8", "pins" : [ { "identity" : "combine-schedulers", "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/combine-schedulers", "state" : { - "revision" : "9dc9cbe4bc45c65164fa653a563d8d8db61b09bb", - "version" : "1.0.0" + "revision" : "9fa31f4403da54855f1e2aeaeff478f4f0e40b13", + "version" : "1.0.2" } }, { @@ -14,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-clocks", "state" : { - "revision" : "d1fd837326aa719bee979bdde1f53cd5797443eb", - "version" : "1.0.0" + "revision" : "b9b24b69e2adda099a1fa381cda1eeec272d5b53", + "version" : "1.0.5" } }, { @@ -23,8 +24,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-concurrency-extras", "state" : { - "revision" : "ea631ce892687f5432a833312292b80db238186a", - "version" : "1.0.0" + "revision" : "163409ef7dae9d960b87f34b51587b6609a76c1f", + "version" : "1.3.0" } }, { @@ -32,8 +33,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-dependencies", "state" : { - "revision" : "4e1eb6e28afe723286d8cc60611237ffbddba7c5", - "version" : "1.0.0" + "revision" : "e2b06090b0b7738fcd8c762131e705d711fc7e8e", + "version" : "1.6.0" + } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-syntax", + "state" : { + "revision" : "0687f71944021d616d34d922343dcef086855920", + "version" : "600.0.1" } }, { @@ -41,10 +51,10 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", "state" : { - "revision" : "302891700c7fa3b92ebde9fe7b42933f8349f3c7", - "version" : "1.0.0" + "revision" : "a3f634d1a409c7979cabc0a71b3f26ffa9fc8af1", + "version" : "1.4.3" } } ], - "version" : 2 + "version" : 3 } diff --git a/Package.swift b/Package.swift index 2a282de..04242c3 100644 --- a/Package.swift +++ b/Package.swift @@ -3,7 +3,7 @@ import PackageDescription let package = Package( - name: "composable-core-location", + name: "core-location-client", platforms: [ .iOS(.v13), .macOS(.v10_15), @@ -12,35 +12,36 @@ let package = Package( ], products: [ .library( - name: "ComposableCoreLocation", - targets: ["ComposableCoreLocation"] + name: "CoreLocationClient", + targets: ["CoreLocationClient"] ) ], dependencies: [ - .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.0.0"), - .package(url: "https://github.com/pointfreeco/swift-concurrency-extras", from: "1.0.0"), + .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.6.0"), + .package(url: "https://github.com/pointfreeco/swift-concurrency-extras", from: "1.3.0"), ], targets: [ .target( - name: "ComposableCoreLocation", + name: "CoreLocationClient", dependencies: [ .product(name: "Dependencies", package: "swift-dependencies"), + .product(name: "DependenciesMacros", package: "swift-dependencies"), .product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"), ] ), .testTarget( - name: "ComposableCoreLocationTests", - dependencies: ["ComposableCoreLocation"] + name: "CoreLocationClientTests", + dependencies: ["CoreLocationClient"] ), ] ) -//for target in package.targets { -// target.swiftSettings = target.swiftSettings ?? [] -// target.swiftSettings?.append( -// .unsafeFlags([ -// "-Xfrontend", "-warn-concurrency", -// "-Xfrontend", "-enable-actor-data-race-checks", -// ]) -// ) -//} +for target in package.targets { + target.swiftSettings = target.swiftSettings ?? [] + target.swiftSettings?.append( + .unsafeFlags([ + "-Xfrontend", "-warn-concurrency", + "-Xfrontend", "-enable-actor-data-race-checks", + ]) + ) +} diff --git a/Package@swift-6.0.swift b/Package@swift-6.0.swift new file mode 100644 index 0000000..d2db212 --- /dev/null +++ b/Package@swift-6.0.swift @@ -0,0 +1,47 @@ +// swift-tools-version:6.0 + +import PackageDescription + +let package = Package( + name: "core-location-client", + platforms: [ + .iOS(.v13), + .macOS(.v10_15), + .tvOS(.v13), + .watchOS(.v6), + ], + products: [ + .library( + name: "CoreLocationClient", + targets: ["CoreLocationClient"] + ) + ], + dependencies: [ + .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.6.0"), + .package(url: "https://github.com/pointfreeco/swift-concurrency-extras", from: "1.3.0"), + ], + targets: [ + .target( + name: "CoreLocationClient", + dependencies: [ + .product(name: "Dependencies", package: "swift-dependencies"), + .product(name: "DependenciesMacros", package: "swift-dependencies"), + .product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"), + ] + ), + .testTarget( + name: "CoreLocationClientTests", + dependencies: ["CoreLocationClient"] + ), + ] +) + +for target in package.targets { + target.swiftSettings = target.swiftSettings ?? [] + target.swiftSettings?.append( + .unsafeFlags([ + "-Xfrontend", "-warn-concurrency", + "-Xfrontend", "-enable-actor-data-race-checks", + ]) + ) +} diff --git a/README.md b/README.md index 4c79e59..bf852ea 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Check out the [LocationManager](./Examples/LocationManager) demo to see Composab To use ComposableCoreLocation in your application, you can add an action to your domain that represents all of the actions the manager can emit via the `CLLocationManagerDelegate` methods: ```swift -import ComposableCoreLocation +import CoreLocationClient enum AppAction { case locationManager(LocationManager.Action) diff --git a/Sources/ComposableCoreLocation/Failing.swift b/Sources/ComposableCoreLocation/Failing.swift deleted file mode 100644 index 6c875ef..0000000 --- a/Sources/ComposableCoreLocation/Failing.swift +++ /dev/null @@ -1,74 +0,0 @@ -import CoreLocation -import XCTestDynamicOverlay - -extension LocationManager { - /// The failing implementation of the ``LocationManager`` interface. By default this - /// implementation stubs all of its endpoints as functions that immediately call `XCTFail`. - /// - /// This allows you to test an even deeper property of your features: that they use only the - /// location manager endpoints that you specify and nothing else. This can be useful as a - /// measurement of just how complex a particular test is. Tests that need to stub many endpoints - /// are in some sense more complicated than tests that only need to stub a few endpoints. It's not - /// necessarily a bad thing to stub many endpoints. Sometimes it's needed. - /// - /// As an example, to create a failing manager that simulates a location manager that has already - /// authorized access to location, and when a location is requested it immediately responds - /// with a mock location we can do something like this: - /// - /// ```swift - /// // Send actions to this subject to simulate the location manager's delegate methods - /// // being called. - /// let locationManagerSubject = PassthroughSubject() - /// - /// // The mock location we want the manager to say we are located at - /// let mockLocation = Location( - /// coordinate: CLLocationCoordinate2D(latitude: 40.6501, longitude: -73.94958), - /// // A whole bunch of other properties have been omitted. - /// ) - /// - /// var manager = LocationManager.failing - /// - /// // Override any CLLocationManager endpoints your test invokes: - /// manager.authorizationStatus = { .authorizedAlways } - /// manager.delegate = { locationManagerSubject.eraseToEffect() } - /// manager.locationServicesEnabled = { true } - /// manager.requestLocation = { - /// .fireAndForget { locationManagerSubject.send(.didUpdateLocations([mockLocation])) } - /// } - /// ``` - public static let failing = Self( - accuracyAuthorization: XCTUnimplemented("\(Self.self).accuracyAuthorization"), - authorizationStatus: XCTUnimplemented("\(Self.self).authorizationStatus"), - delegate: XCTUnimplemented("\(Self.self).delegate"), - dismissHeadingCalibrationDisplay: XCTUnimplemented( - "\(Self.self).dismissHeadingCalibrationDisplay"), - heading: XCTUnimplemented("\(Self.self).heading"), - headingAvailable: XCTUnimplemented("\(Self.self).headingAvailable"), - isRangingAvailable: XCTUnimplemented("\(Self.self).isRangingAvailable"), - location: XCTUnimplemented("\(Self.self).location"), - locationServicesEnabled: XCTUnimplemented("\(Self.self).locationServicesEnabled"), - maximumRegionMonitoringDistance: XCTUnimplemented( - "\(Self.self).maximumRegionMonitoringDistance"), - monitoredRegions: XCTUnimplemented("\(Self.self).monitoredRegions"), - requestAlwaysAuthorization: XCTUnimplemented("\(Self.self).requestAlwaysAuthorization"), - requestLocation: XCTUnimplemented("\(Self.self).requestLocation"), - requestWhenInUseAuthorization: XCTUnimplemented("\(Self.self).requestWhenInUseAuthorization"), - requestTemporaryFullAccuracyAuthorization: XCTUnimplemented( - "\(Self.self).requestTemporaryFullAccuracyAuthorization"), - set: XCTUnimplemented("\(Self.self).set"), - significantLocationChangeMonitoringAvailable: XCTUnimplemented( - "\(Self.self).significantLocationChangeMonitoringAvailable"), - startMonitoringForRegion: XCTUnimplemented("\(Self.self).startMonitoringForRegion"), - startMonitoringSignificantLocationChanges: XCTUnimplemented( - "\(Self.self).startMonitoringSignificantLocationChanges"), - startMonitoringVisits: XCTUnimplemented("\(Self.self).startMonitoringVisits"), - startUpdatingHeading: XCTUnimplemented("\(Self.self).startUpdatingHeading"), - startUpdatingLocation: XCTUnimplemented("\(Self.self).startUpdatingLocation"), - stopMonitoringForRegion: XCTUnimplemented("\(Self.self).stopMonitoringForRegion"), - stopMonitoringSignificantLocationChanges: XCTUnimplemented( - "\(Self.self).stopMonitoringSignificantLocationChanges"), - stopMonitoringVisits: XCTUnimplemented("\(Self.self).stopMonitoringVisits"), - stopUpdatingHeading: XCTUnimplemented("\(Self.self).stopUpdatingHeading"), - stopUpdatingLocation: XCTUnimplemented("\(Self.self).stopUpdatingLocation") - ) -} diff --git a/Sources/ComposableCoreLocation/Interface.swift b/Sources/ComposableCoreLocation/Interface.swift deleted file mode 100644 index acc81ed..0000000 --- a/Sources/ComposableCoreLocation/Interface.swift +++ /dev/null @@ -1,437 +0,0 @@ -import Combine -import CoreLocation - -/// A wrapper around Core Location's `CLLocationManager` that exposes its functionality through -/// effects and actions, making it easy to use with the Composable Architecture and easy to test. -/// -/// To use it, one begins by adding an action to your domain that represents all of the actions the -/// manager can emit via the `CLLocationManagerDelegate` methods: -/// -/// ```swift -/// import ComposableCoreLocation -/// -/// enum AppAction { -/// case locationManager(LocationManager.Action) -/// -/// // Your domain's other actions: -/// ... -/// } -/// ``` -/// -/// The `LocationManager.Action` enum holds a case for each delegate method of -/// `CLLocationManagerDelegate`, such as `didUpdateLocations`, `didEnterRegion`, `didUpdateHeading`, -/// and more. -/// -/// Next we add a `LocationManager`, which is a wrapper around `CLLocationManager` that the library -/// provides, to the application's environment of dependencies: -/// -/// ```swift -/// struct AppEnvironment { -/// var locationManager: LocationManager -/// -/// // Your domain's other dependencies: -/// ... -/// } -/// ``` -/// -/// Then, we simultaneously subscribe to delegate actions and request authorization from our -/// application's reducer by returning an effect from an action to kick things off. One good choice -/// for such an action is the `onAppear` of your view. -/// -/// ```swift -/// let appReducer = Reducer { -/// state, action, environment in -/// -/// switch action { -/// case .onAppear: -/// return .merge( -/// environment.locationManager -/// .delegate() -/// .map(AppAction.locationManager), -/// -/// environment.locationManager -/// .requestWhenInUseAuthorization() -/// .fireAndForget() -/// ) -/// -/// ... -/// } -/// } -/// ``` -/// -/// With that initial setup we will now get all of `CLLocationManagerDelegate`'s delegate methods -/// delivered to our reducer via actions. To handle a particular delegate action we can destructure -/// it inside the `.locationManager` case we added to our `AppAction`. For example, once we get -/// location authorization from the user we could request their current location: -/// -/// ```swift -/// case .locationManager(.didChangeAuthorization(.authorizedAlways)), -/// .locationManager(.didChangeAuthorization(.authorizedWhenInUse)): -/// -/// return environment.locationManager -/// .requestLocation() -/// .fireAndForget() -/// ``` -/// -/// If the user denies location access we can show an alert telling them that we need access to be -/// able to do anything in the app: -/// -/// ```swift -/// case .locationManager(.didChangeAuthorization(.denied)), -/// .locationManager(.didChangeAuthorization(.restricted)): -/// -/// state.alert = """ -/// Please give location access so that we can show you some cool stuff. -/// """ -/// return .none -/// ``` -/// -/// Otherwise, we'll be notified of the user's location by handling the `.didUpdateLocations` -/// action: -/// -/// ```swift -/// case let .locationManager(.didUpdateLocations(locations)): -/// // Do something cool with user's current location. -/// ... -/// ``` -/// -/// Once you have handled all the `CLLocationManagerDelegate` actions you care about, you can ignore -/// the rest: -/// -/// ```swift -/// case .locationManager: -/// return .none -/// ``` -/// -/// And finally, when creating the `Store` to power your application you will supply the "live" -/// implementation of the `LocationManager`, which is an instance that holds onto a -/// `CLLocationManager` on the inside and interacts with it directly: -/// -/// ```swift -/// let store = Store( -/// initialState: AppState(), -/// reducer: appReducer, -/// environment: AppEnvironment( -/// locationManager: .live, -/// // And your other dependencies... -/// ) -/// ) -/// ``` -/// -/// This is enough to implement a basic application that interacts with Core Location. -/// -/// The true power of building your application and interfacing with Core Location in this way is -/// the ability to _test_ how your application interacts with Core Location. It starts by creating -/// a `TestStore` whose environment contains a ``failing`` version of the `LocationManager`. Then, -/// you can selectively override whichever endpoints your feature needs to supply deterministic -/// functionality. -/// -/// For example, to test the flow of asking for location authorization, being denied, and showing an -/// alert, we need to override the `create` and `requestWhenInUseAuthorization` endpoints. The -/// `create` endpoint needs to return an effect that emits the delegate actions, which we can -/// control via a publish subject. And the `requestWhenInUseAuthorization` endpoint is a -/// fire-and-forget effect, but we can make assertions that it was called how we expect. -/// -/// ```swift -/// let store = TestStore( -/// initialState: AppState(), -/// reducer: appReducer, -/// environment: AppEnvironment( -/// locationManager: .failing -/// ) -/// ) -/// -/// var didRequestInUseAuthorization = false -/// let locationManagerSubject = PassthroughSubject() -/// -/// store.environment.locationManager.create = { locationManagerSubject.eraseToEffect() } -/// store.environment.locationManager.requestWhenInUseAuthorization = { -/// .fireAndForget { didRequestInUseAuthorization = true } -/// } -/// ``` -/// -/// Then we can write an assertion that simulates a sequence of user steps and location manager -/// delegate actions, and we can assert against how state mutates and how effects are received. For -/// example, we can have the user come to the screen, deny the location authorization request, and -/// then assert that an effect was received which caused the alert to show: -/// -/// ```swift -/// store.send(.onAppear) -/// -/// // Simulate the user denying location access -/// locationManagerSubject.send(.didChangeAuthorization(.denied)) -/// -/// // We receive the authorization change delegate action from the effect -/// store.receive(.locationManager(.didChangeAuthorization(.denied))) { -/// $0.alert = """ -/// Please give location access so that we can show you some cool stuff. -/// """ -/// -/// // Store assertions require all effects to be completed, so we complete -/// // the subject manually. -/// locationManagerSubject.send(completion: .finished) -/// ``` -/// -/// And this is only the tip of the iceberg. We can further test what happens when we are granted -/// authorization by the user and the request for their location returns a specific location that we -/// control, and even what happens when the request for their location fails. It is very easy to -/// write these tests, and we can test deep, subtle properties of our application. -/// -public struct LocationManager: Sendable { - /// Actions that correspond to `CLLocationManagerDelegate` methods. - /// - /// See `CLLocationManagerDelegate` for more information. - public enum Action: Equatable { - case didChangeAuthorization(CLAuthorizationStatus) - - @available(tvOS, unavailable) - @available(watchOS, unavailable) - case didDetermineState(CLRegionState, region: Region) - - @available(tvOS, unavailable) - @available(watchOS, unavailable) - case didEnterRegion(Region) - - @available(tvOS, unavailable) - @available(watchOS, unavailable) - case didExitRegion(Region) - - @available(macOS, unavailable) - @available(tvOS, unavailable) - @available(watchOS, unavailable) - case didFailRanging(beaconConstraint: CLBeaconIdentityConstraint, error: Error) - - case didFailWithError(Error) - - @available(tvOS, unavailable) - @available(watchOS, unavailable) - case didFinishDeferredUpdatesWithError(Error?) - - @available(tvOS, unavailable) - @available(watchOS, unavailable) - case didPauseLocationUpdates - - @available(tvOS, unavailable) - @available(watchOS, unavailable) - case didResumeLocationUpdates - - @available(tvOS, unavailable) - @available(watchOS, unavailable) - case didStartMonitoring(region: Region) - - @available(macOS, unavailable) - @available(tvOS, unavailable) - case didUpdateHeading(newHeading: Heading) - - case didUpdateLocations([Location]) - - @available(macCatalyst, deprecated: 13) - @available(tvOS, unavailable) - case didUpdateTo(newLocation: Location, oldLocation: Location) - - @available(macOS, unavailable) - @available(tvOS, unavailable) - @available(watchOS, unavailable) - case didVisit(Visit) - - @available(tvOS, unavailable) - @available(watchOS, unavailable) - case monitoringDidFail(region: Region?, error: Error) - - @available(macOS, unavailable) - @available(tvOS, unavailable) - @available(watchOS, unavailable) - case didRangeBeacons([Beacon], satisfyingConstraint: CLBeaconIdentityConstraint) - } - - public struct Error: Swift.Error, Equatable { - public let error: NSError - - public init(_ error: Swift.Error) { - self.error = error as NSError - } - } - - public var accuracyAuthorization: @Sendable () async -> AccuracyAuthorization? - - public var authorizationStatus: @Sendable () async -> CLAuthorizationStatus - - public var delegate: @Sendable () async -> AsyncStream - - public var dismissHeadingCalibrationDisplay: @Sendable () async -> Void - - public var heading: @Sendable () async -> Heading? - - public var headingAvailable: @Sendable () async -> Bool - - public var isRangingAvailable: @Sendable () async -> Bool - - public var location: @Sendable () async -> Location? - - public var locationServicesEnabled: @Sendable () async -> Bool - - public var maximumRegionMonitoringDistance: @Sendable () async -> CLLocationDistance - - public var monitoredRegions: @Sendable () async -> Set - - public var requestAlwaysAuthorization: @Sendable () async -> Void - - public var requestLocation: @Sendable () async -> Void - - public var requestWhenInUseAuthorization: @Sendable () async -> Void - - public var requestTemporaryFullAccuracyAuthorization: @Sendable (String) async throws -> Void - - public var set: @Sendable (Properties) async -> Void - - public var significantLocationChangeMonitoringAvailable: @Sendable () async -> Bool - - public var startMonitoringForRegion: @Sendable (Region) async -> Void - - public var startMonitoringSignificantLocationChanges: @Sendable () async -> Void - - public var startMonitoringVisits: @Sendable () async -> Void - - public var startUpdatingHeading: @Sendable () async -> Void - - public var startUpdatingLocation: @Sendable () async -> Void - - public var stopMonitoringForRegion: @Sendable (Region) async -> Void - - public var stopMonitoringSignificantLocationChanges: @Sendable () async -> Void - - public var stopMonitoringVisits: @Sendable () async -> Void - - public var stopUpdatingHeading: @Sendable () async -> Void - - public var stopUpdatingLocation: @Sendable () async -> Void - - /// Updates the given properties of a uniquely identified `CLLocationManager`. - @Sendable public func set( - activityType: CLActivityType? = nil, - allowsBackgroundLocationUpdates: Bool? = nil, - desiredAccuracy: CLLocationAccuracy? = nil, - distanceFilter: CLLocationDistance? = nil, - headingFilter: CLLocationDegrees? = nil, - headingOrientation: CLDeviceOrientation? = nil, - pausesLocationUpdatesAutomatically: Bool? = nil, - showsBackgroundLocationIndicator: Bool? = nil - ) async { - #if os(macOS) || os(tvOS) || os(watchOS) - #else - await self.set( - Properties( - activityType: activityType, - allowsBackgroundLocationUpdates: allowsBackgroundLocationUpdates, - desiredAccuracy: desiredAccuracy, - distanceFilter: distanceFilter, - headingFilter: headingFilter, - headingOrientation: headingOrientation, - pausesLocationUpdatesAutomatically: pausesLocationUpdatesAutomatically, - showsBackgroundLocationIndicator: showsBackgroundLocationIndicator - ) - ) - #endif - } -} - -extension LocationManager { - public struct Properties: Equatable { - var activityType: CLActivityType? = nil - - var allowsBackgroundLocationUpdates: Bool? = nil - - var desiredAccuracy: CLLocationAccuracy? = nil - - var distanceFilter: CLLocationDistance? = nil - - var headingFilter: CLLocationDegrees? = nil - - var headingOrientation: CLDeviceOrientation? = nil - - var pausesLocationUpdatesAutomatically: Bool? = nil - - var showsBackgroundLocationIndicator: Bool? = nil - - public static func == (lhs: Self, rhs: Self) -> Bool { - var isEqual = true -#if os(iOS) || targetEnvironment(macCatalyst) || os(watchOS) - isEqual = - isEqual - && lhs.activityType == rhs.activityType - && lhs.allowsBackgroundLocationUpdates == rhs.allowsBackgroundLocationUpdates -#endif - isEqual = - isEqual - && lhs.desiredAccuracy == rhs.desiredAccuracy - && lhs.distanceFilter == rhs.distanceFilter -#if os(iOS) || targetEnvironment(macCatalyst) || os(watchOS) - isEqual = - isEqual - && lhs.headingFilter == rhs.headingFilter - && lhs.headingOrientation == rhs.headingOrientation -#endif -#if os(iOS) || targetEnvironment(macCatalyst) - isEqual = - isEqual - && lhs.pausesLocationUpdatesAutomatically == rhs.pausesLocationUpdatesAutomatically - && lhs.showsBackgroundLocationIndicator == rhs.showsBackgroundLocationIndicator -#endif - return isEqual - } - - @available(macOS, unavailable) - @available(tvOS, unavailable) - @available(watchOS, unavailable) - public init( - activityType: CLActivityType? = nil, - allowsBackgroundLocationUpdates: Bool? = nil, - desiredAccuracy: CLLocationAccuracy? = nil, - distanceFilter: CLLocationDistance? = nil, - headingFilter: CLLocationDegrees? = nil, - headingOrientation: CLDeviceOrientation? = nil, - pausesLocationUpdatesAutomatically: Bool? = nil, - showsBackgroundLocationIndicator: Bool? = nil - ) { - self.activityType = activityType - self.allowsBackgroundLocationUpdates = allowsBackgroundLocationUpdates - self.desiredAccuracy = desiredAccuracy - self.distanceFilter = distanceFilter - self.headingFilter = headingFilter - self.headingOrientation = headingOrientation - self.pausesLocationUpdatesAutomatically = pausesLocationUpdatesAutomatically - self.showsBackgroundLocationIndicator = showsBackgroundLocationIndicator - } - - @available(iOS, unavailable) - @available(macCatalyst, unavailable) - @available(watchOS, unavailable) - public init( - desiredAccuracy: CLLocationAccuracy? = nil, - distanceFilter: CLLocationDistance? = nil - ) { - self.desiredAccuracy = desiredAccuracy - self.distanceFilter = distanceFilter - } - - @available(iOS, unavailable) - @available(macCatalyst, unavailable) - @available(macOS, unavailable) - @available(tvOS, unavailable) - public init( - activityType: CLActivityType? = nil, - allowsBackgroundLocationUpdates: Bool? = nil, - desiredAccuracy: CLLocationAccuracy? = nil, - distanceFilter: CLLocationDistance? = nil, - headingFilter: CLLocationDegrees? = nil, - headingOrientation: CLDeviceOrientation? = nil - ) { - self.activityType = activityType - self.allowsBackgroundLocationUpdates = allowsBackgroundLocationUpdates - self.desiredAccuracy = desiredAccuracy - self.distanceFilter = distanceFilter - self.headingFilter = headingFilter - self.headingOrientation = headingOrientation - } - } -} diff --git a/Sources/ComposableCoreLocation/Dependencies.swift b/Sources/CoreLocationClient/Dependencies.swift similarity index 73% rename from Sources/ComposableCoreLocation/Dependencies.swift rename to Sources/CoreLocationClient/Dependencies.swift index 5f0ffa3..5e49380 100644 --- a/Sources/ComposableCoreLocation/Dependencies.swift +++ b/Sources/CoreLocationClient/Dependencies.swift @@ -8,6 +8,6 @@ extension DependencyValues { } extension LocationManager: DependencyKey { - public static let testValue = Self.failing - public static var liveValue = Self.live + public static let testValue = Self() + public static let liveValue = Self.live } diff --git a/Sources/CoreLocationClient/Interface.swift b/Sources/CoreLocationClient/Interface.swift new file mode 100644 index 0000000..c463c0e --- /dev/null +++ b/Sources/CoreLocationClient/Interface.swift @@ -0,0 +1,439 @@ +import CoreLocation +import Dependencies +import DependenciesMacros + +/// A wrapper around Core Location's `CLLocationManager` that exposes its functionality through +/// effects and actions, making it easy to use with the Composable Architecture and easy to test. +/// +/// To use it, one begins by adding an action to your domain that represents all of the actions the +/// manager can emit via the `CLLocationManagerDelegate` methods: +/// +/// ```swift +/// import CoreLocationClient +/// +/// enum AppAction { +/// case locationManager(LocationManager.Action) +/// +/// // Your domain's other actions: +/// ... +/// } +/// ``` +/// +/// The `LocationManager.Action` enum holds a case for each delegate method of +/// `CLLocationManagerDelegate`, such as `didUpdateLocations`, `didEnterRegion`, `didUpdateHeading`, +/// and more. +/// +/// Next we add a `LocationManager`, which is a wrapper around `CLLocationManager` that the library +/// provides, to the application's environment of dependencies: +/// +/// ```swift +/// struct AppEnvironment { +/// var locationManager: LocationManager +/// +/// // Your domain's other dependencies: +/// ... +/// } +/// ``` +/// +/// Then, we simultaneously subscribe to delegate actions and request authorization from our +/// application's reducer by returning an effect from an action to kick things off. One good choice +/// for such an action is the `onAppear` of your view. +/// +/// ```swift +/// let appReducer = Reducer { +/// state, action, environment in +/// +/// switch action { +/// case .onAppear: +/// return .merge( +/// environment.locationManager +/// .delegate() +/// .map(AppAction.locationManager), +/// +/// environment.locationManager +/// .requestWhenInUseAuthorization() +/// .fireAndForget() +/// ) +/// +/// ... +/// } +/// } +/// ``` +/// +/// With that initial setup we will now get all of `CLLocationManagerDelegate`'s delegate methods +/// delivered to our reducer via actions. To handle a particular delegate action we can destructure +/// it inside the `.locationManager` case we added to our `AppAction`. For example, once we get +/// location authorization from the user we could request their current location: +/// +/// ```swift +/// case .locationManager(.didChangeAuthorization(.authorizedAlways)), +/// .locationManager(.didChangeAuthorization(.authorizedWhenInUse)): +/// +/// return environment.locationManager +/// .requestLocation() +/// .fireAndForget() +/// ``` +/// +/// If the user denies location access we can show an alert telling them that we need access to be +/// able to do anything in the app: +/// +/// ```swift +/// case .locationManager(.didChangeAuthorization(.denied)), +/// .locationManager(.didChangeAuthorization(.restricted)): +/// +/// state.alert = """ +/// Please give location access so that we can show you some cool stuff. +/// """ +/// return .none +/// ``` +/// +/// Otherwise, we'll be notified of the user's location by handling the `.didUpdateLocations` +/// action: +/// +/// ```swift +/// case let .locationManager(.didUpdateLocations(locations)): +/// // Do something cool with user's current location. +/// ... +/// ``` +/// +/// Once you have handled all the `CLLocationManagerDelegate` actions you care about, you can ignore +/// the rest: +/// +/// ```swift +/// case .locationManager: +/// return .none +/// ``` +/// +/// And finally, when creating the `Store` to power your application you will supply the "live" +/// implementation of the `LocationManager`, which is an instance that holds onto a +/// `CLLocationManager` on the inside and interacts with it directly: +/// +/// ```swift +/// let store = Store( +/// initialState: AppState(), +/// reducer: appReducer, +/// environment: AppEnvironment( +/// locationManager: .live, +/// // And your other dependencies... +/// ) +/// ) +/// ``` +/// +/// This is enough to implement a basic application that interacts with Core Location. +/// +/// The true power of building your application and interfacing with Core Location in this way is +/// the ability to _test_ how your application interacts with Core Location. It starts by creating +/// a `TestStore` whose environment contains a ``failing`` version of the `LocationManager`. Then, +/// you can selectively override whichever endpoints your feature needs to supply deterministic +/// functionality. +/// +/// For example, to test the flow of asking for location authorization, being denied, and showing an +/// alert, we need to override the `create` and `requestWhenInUseAuthorization` endpoints. The +/// `create` endpoint needs to return an effect that emits the delegate actions, which we can +/// control via a publish subject. And the `requestWhenInUseAuthorization` endpoint is a +/// fire-and-forget effect, but we can make assertions that it was called how we expect. +/// +/// ```swift +/// let store = TestStore( +/// initialState: AppState(), +/// reducer: appReducer, +/// environment: AppEnvironment( +/// locationManager: .failing +/// ) +/// ) +/// +/// var didRequestInUseAuthorization = false +/// let locationManagerSubject = PassthroughSubject() +/// +/// store.environment.locationManager.create = { locationManagerSubject.eraseToEffect() } +/// store.environment.locationManager.requestWhenInUseAuthorization = { +/// .fireAndForget { didRequestInUseAuthorization = true } +/// } +/// ``` +/// +/// Then we can write an assertion that simulates a sequence of user steps and location manager +/// delegate actions, and we can assert against how state mutates and how effects are received. For +/// example, we can have the user come to the screen, deny the location authorization request, and +/// then assert that an effect was received which caused the alert to show: +/// +/// ```swift +/// store.send(.onAppear) +/// +/// // Simulate the user denying location access +/// locationManagerSubject.send(.didChangeAuthorization(.denied)) +/// +/// // We receive the authorization change delegate action from the effect +/// store.receive(.locationManager(.didChangeAuthorization(.denied))) { +/// $0.alert = """ +/// Please give location access so that we can show you some cool stuff. +/// """ +/// +/// // Store assertions require all effects to be completed, so we complete +/// // the subject manually. +/// locationManagerSubject.send(completion: .finished) +/// ``` +/// +/// And this is only the tip of the iceberg. We can further test what happens when we are granted +/// authorization by the user and the request for their location returns a specific location that we +/// control, and even what happens when the request for their location fails. It is very easy to +/// write these tests, and we can test deep, subtle properties of our application. +/// +@DependencyClient +public struct LocationManager: Sendable { + /// Actions that correspond to `CLLocationManagerDelegate` methods. + /// + /// See `CLLocationManagerDelegate` for more information. + public enum Action: Equatable, Sendable { + case didChangeAuthorization(CLAuthorizationStatus) + + @available(tvOS, unavailable) + @available(watchOS, unavailable) + case didDetermineState(CLRegionState, region: Region) + + @available(tvOS, unavailable) + @available(watchOS, unavailable) + case didEnterRegion(Region) + + @available(tvOS, unavailable) + @available(watchOS, unavailable) + case didExitRegion(Region) + + @available(macOS, unavailable) + @available(tvOS, unavailable) + @available(watchOS, unavailable) + case didFailRanging(beaconConstraint: CLBeaconIdentityConstraint, error: Error) + + case didFailWithError(Error) + + @available(tvOS, unavailable) + @available(watchOS, unavailable) + case didFinishDeferredUpdatesWithError(Error?) + + @available(tvOS, unavailable) + @available(watchOS, unavailable) + case didPauseLocationUpdates + + @available(tvOS, unavailable) + @available(watchOS, unavailable) + case didResumeLocationUpdates + + @available(tvOS, unavailable) + @available(watchOS, unavailable) + case didStartMonitoring(region: Region) + + @available(macOS, unavailable) + @available(tvOS, unavailable) + case didUpdateHeading(newHeading: Heading) + + case didUpdateLocations([Location]) + + @available(macCatalyst, deprecated: 13) + @available(tvOS, unavailable) + case didUpdateTo(newLocation: Location, oldLocation: Location) + + @available(macOS, unavailable) + @available(tvOS, unavailable) + @available(watchOS, unavailable) + case didVisit(Visit) + + @available(tvOS, unavailable) + @available(watchOS, unavailable) + case monitoringDidFail(region: Region?, error: Error) + + @available(macOS, unavailable) + @available(tvOS, unavailable) + @available(watchOS, unavailable) + case didRangeBeacons([Beacon], satisfyingConstraint: CLBeaconIdentityConstraint) + } + + public struct Error: Swift.Error, Equatable { + public let error: NSError + + public init(_ error: Swift.Error) { + self.error = error as NSError + } + } + + public var accuracyAuthorization: @Sendable () async -> AccuracyAuthorization? + + public var authorizationStatus: @Sendable () async -> CLAuthorizationStatus = { .notDetermined } + + public var delegate: @Sendable () async -> AsyncStream = { .never } + + public var dismissHeadingCalibrationDisplay: @Sendable () async -> Void + + public var heading: @Sendable () async -> Heading? + + public var headingAvailable: @Sendable () async -> Bool = { false } + + public var isRangingAvailable: @Sendable () async -> Bool = { false } + + public var location: @Sendable () async -> Location? + + public var locationServicesEnabled: @Sendable () async -> Bool = { false } + + public var maximumRegionMonitoringDistance: @Sendable () async -> CLLocationDistance = { .nan } + + public var monitoredRegions: @Sendable () async -> Set = { .init() } + + public var requestAlwaysAuthorization: @Sendable () async -> Void + + public var requestLocation: @Sendable () async -> Void + + public var requestWhenInUseAuthorization: @Sendable () async -> Void + + public var requestTemporaryFullAccuracyAuthorization: @Sendable (String) async throws -> Void + + public var set: @Sendable (Properties) async -> Void + + public var significantLocationChangeMonitoringAvailable: @Sendable () async -> Bool = { false } + + public var startMonitoringForRegion: @Sendable (Region) async -> Void + + public var startMonitoringSignificantLocationChanges: @Sendable () async -> Void + + public var startMonitoringVisits: @Sendable () async -> Void + + public var startUpdatingHeading: @Sendable () async -> Void + + public var startUpdatingLocation: @Sendable () async -> Void + + public var stopMonitoringForRegion: @Sendable (Region) async -> Void + + public var stopMonitoringSignificantLocationChanges: @Sendable () async -> Void + + public var stopMonitoringVisits: @Sendable () async -> Void + + public var stopUpdatingHeading: @Sendable () async -> Void + + public var stopUpdatingLocation: @Sendable () async -> Void + + /// Updates the given properties of a uniquely identified `CLLocationManager`. + @Sendable public func set( + activityType: CLActivityType? = nil, + allowsBackgroundLocationUpdates: Bool? = nil, + desiredAccuracy: CLLocationAccuracy? = nil, + distanceFilter: CLLocationDistance? = nil, + headingFilter: CLLocationDegrees? = nil, + headingOrientation: CLDeviceOrientation? = nil, + pausesLocationUpdatesAutomatically: Bool? = nil, + showsBackgroundLocationIndicator: Bool? = nil + ) async { +#if os(macOS) || os(tvOS) || os(watchOS) +#else + await self.set( + Properties( + activityType: activityType, + allowsBackgroundLocationUpdates: allowsBackgroundLocationUpdates, + desiredAccuracy: desiredAccuracy, + distanceFilter: distanceFilter, + headingFilter: headingFilter, + headingOrientation: headingOrientation, + pausesLocationUpdatesAutomatically: pausesLocationUpdatesAutomatically, + showsBackgroundLocationIndicator: showsBackgroundLocationIndicator + ) + ) +#endif + } +} + +extension LocationManager { + public struct Properties: Equatable { + var activityType: CLActivityType? = nil + + var allowsBackgroundLocationUpdates: Bool? = nil + + var desiredAccuracy: CLLocationAccuracy? = nil + + var distanceFilter: CLLocationDistance? = nil + + var headingFilter: CLLocationDegrees? = nil + + var headingOrientation: CLDeviceOrientation? = nil + + var pausesLocationUpdatesAutomatically: Bool? = nil + + var showsBackgroundLocationIndicator: Bool? = nil + + public static func == (lhs: Self, rhs: Self) -> Bool { + var isEqual = true +#if os(iOS) || targetEnvironment(macCatalyst) || os(watchOS) + isEqual = + isEqual + && lhs.activityType == rhs.activityType + && lhs.allowsBackgroundLocationUpdates == rhs.allowsBackgroundLocationUpdates +#endif + isEqual = + isEqual + && lhs.desiredAccuracy == rhs.desiredAccuracy + && lhs.distanceFilter == rhs.distanceFilter +#if os(iOS) || targetEnvironment(macCatalyst) || os(watchOS) + isEqual = + isEqual + && lhs.headingFilter == rhs.headingFilter + && lhs.headingOrientation == rhs.headingOrientation +#endif +#if os(iOS) || targetEnvironment(macCatalyst) + isEqual = + isEqual + && lhs.pausesLocationUpdatesAutomatically == rhs.pausesLocationUpdatesAutomatically + && lhs.showsBackgroundLocationIndicator == rhs.showsBackgroundLocationIndicator +#endif + return isEqual + } + + @available(macOS, unavailable) + @available(tvOS, unavailable) + @available(watchOS, unavailable) + public init( + activityType: CLActivityType? = nil, + allowsBackgroundLocationUpdates: Bool? = nil, + desiredAccuracy: CLLocationAccuracy? = nil, + distanceFilter: CLLocationDistance? = nil, + headingFilter: CLLocationDegrees? = nil, + headingOrientation: CLDeviceOrientation? = nil, + pausesLocationUpdatesAutomatically: Bool? = nil, + showsBackgroundLocationIndicator: Bool? = nil + ) { + self.activityType = activityType + self.allowsBackgroundLocationUpdates = allowsBackgroundLocationUpdates + self.desiredAccuracy = desiredAccuracy + self.distanceFilter = distanceFilter + self.headingFilter = headingFilter + self.headingOrientation = headingOrientation + self.pausesLocationUpdatesAutomatically = pausesLocationUpdatesAutomatically + self.showsBackgroundLocationIndicator = showsBackgroundLocationIndicator + } + + @available(iOS, unavailable) + @available(macCatalyst, unavailable) + @available(watchOS, unavailable) + public init( + desiredAccuracy: CLLocationAccuracy? = nil, + distanceFilter: CLLocationDistance? = nil + ) { + self.desiredAccuracy = desiredAccuracy + self.distanceFilter = distanceFilter + } + + @available(iOS, unavailable) + @available(macCatalyst, unavailable) + @available(macOS, unavailable) + @available(tvOS, unavailable) + public init( + activityType: CLActivityType? = nil, + allowsBackgroundLocationUpdates: Bool? = nil, + desiredAccuracy: CLLocationAccuracy? = nil, + distanceFilter: CLLocationDistance? = nil, + headingFilter: CLLocationDegrees? = nil, + headingOrientation: CLDeviceOrientation? = nil + ) { + self.activityType = activityType + self.allowsBackgroundLocationUpdates = allowsBackgroundLocationUpdates + self.desiredAccuracy = desiredAccuracy + self.distanceFilter = distanceFilter + self.headingFilter = headingFilter + self.headingOrientation = headingOrientation + } + } +} diff --git a/Sources/ComposableCoreLocation/Internal/Deprecations.swift b/Sources/CoreLocationClient/Internal/Deprecations.swift similarity index 100% rename from Sources/ComposableCoreLocation/Internal/Deprecations.swift rename to Sources/CoreLocationClient/Internal/Deprecations.swift diff --git a/Sources/ComposableCoreLocation/Internal/Exports.swift b/Sources/CoreLocationClient/Internal/Exports.swift similarity index 100% rename from Sources/ComposableCoreLocation/Internal/Exports.swift rename to Sources/CoreLocationClient/Internal/Exports.swift diff --git a/Sources/ComposableCoreLocation/Live.swift b/Sources/CoreLocationClient/Live.swift similarity index 97% rename from Sources/ComposableCoreLocation/Live.swift rename to Sources/CoreLocationClient/Live.swift index d69f234..f319817 100644 --- a/Sources/ComposableCoreLocation/Live.swift +++ b/Sources/CoreLocationClient/Live.swift @@ -212,7 +212,7 @@ private struct LocationManagerSendableBox: Sendable { } private final class LocationManagerDelegate: NSObject, CLLocationManagerDelegate, Sendable { - let continuations: ActorIsolated<[UUID: AsyncStream.Continuation]> + let continuations: LockIsolated<[UUID: AsyncStream.Continuation]> override init() { self.continuations = .init([:]) @@ -221,7 +221,7 @@ private final class LocationManagerDelegate: NSObject, CLLocationManagerDelegate func registerContinuation(_ continuation: AsyncStream.Continuation) { Task { [continuations] in - await continuations.withValue { + continuations.withValue { let id = UUID() $0[id] = continuation continuation.onTermination = { [weak self] _ in self?.unregisterContinuation(withID: id) } @@ -230,12 +230,12 @@ private final class LocationManagerDelegate: NSObject, CLLocationManagerDelegate } private func unregisterContinuation(withID id: UUID) { - Task { [continuations] in await continuations.withValue { $0.removeValue(forKey: id) } } + Task { [continuations] in continuations.withValue { $0.removeValue(forKey: id) } } } private func send(_ action: LocationManager.Action) { Task { [continuations] in - await continuations.withValue { $0.values.forEach { $0.yield(action) } } + continuations.withValue { $0.values.forEach { $0.yield(action) } } } } diff --git a/Sources/ComposableCoreLocation/Models/AccuracyAuthorization.swift b/Sources/CoreLocationClient/Models/AccuracyAuthorization.swift similarity index 100% rename from Sources/ComposableCoreLocation/Models/AccuracyAuthorization.swift rename to Sources/CoreLocationClient/Models/AccuracyAuthorization.swift diff --git a/Sources/ComposableCoreLocation/Models/Beacon.swift b/Sources/CoreLocationClient/Models/Beacon.swift similarity index 100% rename from Sources/ComposableCoreLocation/Models/Beacon.swift rename to Sources/CoreLocationClient/Models/Beacon.swift diff --git a/Sources/ComposableCoreLocation/Models/BeaconConstraint.swift b/Sources/CoreLocationClient/Models/BeaconConstraint.swift similarity index 100% rename from Sources/ComposableCoreLocation/Models/BeaconConstraint.swift rename to Sources/CoreLocationClient/Models/BeaconConstraint.swift diff --git a/Sources/ComposableCoreLocation/Models/Heading.swift b/Sources/CoreLocationClient/Models/Heading.swift similarity index 100% rename from Sources/ComposableCoreLocation/Models/Heading.swift rename to Sources/CoreLocationClient/Models/Heading.swift diff --git a/Sources/ComposableCoreLocation/Models/Location.swift b/Sources/CoreLocationClient/Models/Location.swift similarity index 100% rename from Sources/ComposableCoreLocation/Models/Location.swift rename to Sources/CoreLocationClient/Models/Location.swift diff --git a/Sources/ComposableCoreLocation/Models/Region.swift b/Sources/CoreLocationClient/Models/Region.swift similarity index 100% rename from Sources/ComposableCoreLocation/Models/Region.swift rename to Sources/CoreLocationClient/Models/Region.swift diff --git a/Sources/ComposableCoreLocation/Models/Visit.swift b/Sources/CoreLocationClient/Models/Visit.swift similarity index 100% rename from Sources/ComposableCoreLocation/Models/Visit.swift rename to Sources/CoreLocationClient/Models/Visit.swift diff --git a/Tests/ComposableCoreLocationTests/ComposableCoreLocationTests.swift b/Tests/CoreLocationClientTests/ComposableCoreLocationTests.swift similarity index 99% rename from Tests/ComposableCoreLocationTests/ComposableCoreLocationTests.swift rename to Tests/CoreLocationClientTests/ComposableCoreLocationTests.swift index 82dd44d..d8c2179 100644 --- a/Tests/ComposableCoreLocationTests/ComposableCoreLocationTests.swift +++ b/Tests/CoreLocationClientTests/ComposableCoreLocationTests.swift @@ -1,4 +1,4 @@ -import ComposableCoreLocation +import CoreLocationClient import XCTest class ComposableCoreLocationTests: XCTestCase {