├── .gitignore ├── assets ├── swiftlocation-dark.png ├── swiftlocation-light.png └── swiftlocation-src.sketch ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── Package.swift ├── .github ├── PULL_REQUEST_TEMPLATE │ ├── Improvement.md │ └── New_Feature.md ├── ISSUE_TEMPLATE │ ├── feature.yml │ └── bug.yml └── workflows │ └── ci.yml ├── Scripts └── test.sh ├── LICENSE ├── Sources └── SwiftLocation │ ├── SwiftLocation.swift │ ├── Async Tasks │ ├── AnyTask.swift │ ├── Authorization.swift │ ├── LocationServicesEnabled.swift │ ├── AccuracyAuthorization.swift │ ├── VisitsMonitoring.swift │ ├── HeadingMonitoring.swift │ ├── BeaconMonitoring.swift │ ├── SingleUpdateLocation.swift │ ├── SignificantLocationMonitoring.swift │ ├── AccuracyPermission.swift │ ├── LocatePermission.swift │ ├── RegionMonitoring.swift │ └── ContinuousUpdateLocation.swift │ ├── Support │ ├── LocationErrors.swift │ ├── LocationDelegate.swift │ ├── Extensions.swift │ └── SupportModels.swift │ ├── Location Managers │ ├── LocationManagerBridgeEvent.swift │ ├── LocationAsyncBridge.swift │ └── LocationManagerProtocol.swift │ └── Location.swift ├── CONTRIBUTING.md ├── Tests └── SwiftLocationTests │ ├── MockedLocationManager.swift │ └── SwiftLocationTests.swift └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | -------------------------------------------------------------------------------- /assets/swiftlocation-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malcommac/SwiftLocation/HEAD/assets/swiftlocation-dark.png -------------------------------------------------------------------------------- /assets/swiftlocation-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malcommac/SwiftLocation/HEAD/assets/swiftlocation-light.png -------------------------------------------------------------------------------- /assets/swiftlocation-src.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malcommac/SwiftLocation/HEAD/assets/swiftlocation-src.sketch -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.5 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: "SwiftLocation", 8 | platforms: [.iOS(.v14), .macOS(.v11), .watchOS(.v7), .tvOS(.v14)], 9 | products: [ 10 | .library( 11 | name: "SwiftLocation", 12 | targets: ["SwiftLocation"]), 13 | ], 14 | targets: [ 15 | .target( 16 | name: "SwiftLocation"), 17 | .testTarget( 18 | name: "SwiftLocationTests", 19 | dependencies: ["SwiftLocation"]), 20 | ] 21 | ) 22 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/Improvement.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: ⚙ Improvement 3 | about: You have some improvement to make the library better? 🎁 4 | --- 5 | 6 | 11 | 12 | ### Improvement 13 | 14 | 15 | 16 | | Q | A 17 | |------------ | ------ 18 | | New Feature | yes 19 | | RFC | yes/no 20 | | BC Break | yes/no 21 | | Issue | Close #... 22 | 23 | #### Summary 24 | 25 | 26 | -------------------------------------------------------------------------------- /Scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eo pipefail 4 | 5 | scheme="SwiftLocation" 6 | 7 | while getopts "s:d:" opt; do 8 | case $opt in 9 | s) scheme=${OPTARG};; 10 | d) destinations+=("$OPTARG");; 11 | #... 12 | esac 13 | done 14 | shift $((OPTIND -1)) 15 | 16 | echo "scheme = ${scheme}" 17 | echo "destinations = ${destinations[@]}" 18 | 19 | xcodebuild -version 20 | 21 | xcodebuild build-for-testing -scheme "$scheme" -destination "${destinations[0]}" | xcpretty 22 | 23 | for destination in "${destinations[@]}"; 24 | do 25 | echo "\nRunning tests for destination: $destination" 26 | xcodebuild test-without-building -scheme "$scheme" -destination "$destination" | xcpretty --test 27 | done 28 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/New_Feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🎉 New Feature 3 | about: You have implemented some neat idea that you want to make part of the library? 🎩 4 | --- 5 | 6 | 11 | 12 | ### New Feature 13 | 14 | 15 | 16 | | Q | A 17 | |------------ | ------ 18 | | New Feature | yes 19 | | RFC | yes/no 20 | | BC Break | yes/no 21 | | Issue | Close #... 22 | 23 | #### Summary 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.yml: -------------------------------------------------------------------------------- 1 | name: ✅ Feature Request 2 | description: Request New SDK Features 3 | title: '[Feature]: ' 4 | labels: ["feature-request"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thank you for taking the time to improve this project! 10 | - type: textarea 11 | id: problem 12 | attributes: 13 | label: What problem are you facing? 14 | description: Help us understand what you're unable to accomplish, or what's difficult with your integration 15 | placeholder: | 16 | ex: I am unable to accomplish XYZ today, since the SDK does not allow me to... 17 | validations: 18 | required: true 19 | - type: textarea 20 | id: other_information 21 | attributes: 22 | label: Other Information 23 | description: Any additional information you'd like to share? 24 | validations: 25 | required: false 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Copyright (c) 2023 Daniele Margutti 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: "SwiftLocation CI" 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - '*' 10 | 11 | concurrency: 12 | group: ci 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | ios-latest: 17 | name: Unit Tests (iOS 16.4, Xcode 14.3.1) 18 | runs-on: macOS-13 19 | env: 20 | DEVELOPER_DIR: /Applications/Xcode_14.3.1.app/Contents/Developer 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Run Tests 24 | run: | 25 | Scripts/test.sh -s "SwiftLocation" -d "OS=16.4,name=iPhone 14 Pro" 26 | # macos-latest: 27 | # name: Unit Tests (macOS, Xcode 14.3.1) 28 | # runs-on: macOS-13 29 | # env: 30 | # DEVELOPER_DIR: /Applications/Xcode_14.3.1.app/Contents/Developer 31 | # steps: 32 | # - uses: actions/checkout@v2 33 | # - name: Run Tests 34 | # run: Scripts/test.sh -d "platform=macOS" 35 | tvos-latest: 36 | name: Unit Tests (tvOS 16.4, Xcode 14.3.1) 37 | runs-on: macOS-13 38 | env: 39 | DEVELOPER_DIR: /Applications/Xcode_14.3.1.app/Contents/Developer 40 | steps: 41 | - uses: actions/checkout@v2 42 | - name: Run Tests 43 | run: | 44 | Scripts/test.sh -s "SwiftLocation" -d "OS=16.4,name=Apple TV" 45 | -------------------------------------------------------------------------------- /Sources/SwiftLocation/SwiftLocation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftLocation 3 | // Async/Await Wrapper for CoreLocation 4 | // 5 | // Copyright (c) 2023 Daniele Margutti (hello@danielemargutti.com). 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | import Foundation 27 | 28 | public class SwiftLocationVersion { 29 | 30 | /// Version of the SDK. 31 | public static let version = "6.0.1" 32 | 33 | } 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | name: 🐞 Bug report 2 | description: File a Bug Report for unexpected or incorrect SDK Behavior 3 | title: '[Bug]: ' 4 | labels: ["bug"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to fill out this bug report! 10 | - type: input 11 | id: platform_version 12 | attributes: 13 | label: Platform Version 14 | placeholder: ex. iOS 15.4 15 | validations: 16 | required: true 17 | - type: input 18 | id: sdk_version 19 | attributes: 20 | label: SDK Version 21 | placeholder: ex. 1.7.0 22 | validations: 23 | required: true 24 | - type: input 25 | id: xcode_version 26 | attributes: 27 | label: Xcode Version 28 | placeholder: ex. Xcode 13.3 29 | validations: 30 | required: true 31 | - type: textarea 32 | id: repro_steps 33 | attributes: 34 | label: Steps To Reproduce 35 | description: Please provide a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example) 36 | validations: 37 | required: true 38 | - type: textarea 39 | id: expected_behavior 40 | attributes: 41 | label: Expected Behavior 42 | description: What was supposed to happen? 43 | validations: 44 | required: true 45 | - type: textarea 46 | id: actual_behavior 47 | attributes: 48 | label: Actual Incorrect Behavior 49 | description: What incorrect behavior happened instead? 50 | validations: 51 | required: true 52 | -------------------------------------------------------------------------------- /Sources/SwiftLocation/Async Tasks/AnyTask.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftLocation 3 | // Async/Await Wrapper for CoreLocation 4 | // 5 | // Copyright (c) 2023 Daniele Margutti (hello@danielemargutti.com). 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | import Foundation 27 | 28 | public enum Tasks { } 29 | 30 | public protocol AnyTask: AnyObject { 31 | 32 | var cancellable: CancellableTask? { get set } 33 | var uuid: UUID { get } 34 | var taskType: ObjectIdentifier { get } 35 | 36 | func receivedLocationManagerEvent(_ event: LocationManagerBridgeEvent) 37 | func didCancelled() 38 | func willStart() 39 | 40 | } 41 | 42 | public extension AnyTask { 43 | 44 | var taskType: ObjectIdentifier { 45 | ObjectIdentifier(Self.self) 46 | } 47 | 48 | func didCancelled() { } 49 | func willStart() { } 50 | 51 | } 52 | 53 | 54 | public protocol CancellableTask: AnyObject { 55 | 56 | func cancel(task: any AnyTask) 57 | 58 | } 59 | -------------------------------------------------------------------------------- /Sources/SwiftLocation/Support/LocationErrors.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftLocation 3 | // Async/Await Wrapper for CoreLocation 4 | // 5 | // Copyright (c) 2023 Daniele Margutti (hello@danielemargutti.com). 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | import Foundation 27 | 28 | /// Throwable errors 29 | enum LocationErrors: LocalizedError { 30 | 31 | /// Info.plist authorization are not correctly defined. 32 | case plistNotConfigured 33 | 34 | /// System location services are disabled by the user or not available. 35 | case locationServicesDisabled 36 | 37 | /// You must require location authorization from the user before executing the operation. 38 | case authorizationRequired 39 | 40 | /// Not authorized by the user. 41 | case notAuthorized 42 | 43 | /// Operation timeout. 44 | case timeout 45 | 46 | var errorDescription: String? { 47 | switch self { 48 | case .plistNotConfigured: 49 | return "Missing authorization into Info.plist" 50 | case .locationServicesDisabled: 51 | return "Location services disabled/not available" 52 | case .authorizationRequired: 53 | return "Location authorization not requested yet" 54 | case .notAuthorized: 55 | return "Not Authorized" 56 | case .timeout: 57 | return "Timeout" 58 | } 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /Sources/SwiftLocation/Async Tasks/Authorization.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftLocation 3 | // Async/Await Wrapper for CoreLocation 4 | // 5 | // Copyright (c) 2023 Daniele Margutti (hello@danielemargutti.com). 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | import Foundation 27 | import CoreLocation 28 | 29 | extension Tasks { 30 | 31 | public final class Authorization: AnyTask { 32 | 33 | // MARK: - Support Structures 34 | 35 | /// Stream produced by the task. 36 | public typealias Stream = AsyncStream 37 | 38 | /// The event produced by the stream. 39 | public enum StreamEvent { 40 | 41 | /// Authorization did change with a new value 42 | case didChangeAuthorization(_ status: CLAuthorizationStatus) 43 | 44 | /// The current status of the authorization. 45 | public var authorizationStatus: CLAuthorizationStatus { 46 | switch self { 47 | case let .didChangeAuthorization(status): 48 | return status 49 | } 50 | } 51 | 52 | } 53 | 54 | // MARK: - Public Properties 55 | 56 | public let uuid = UUID() 57 | public var stream: Stream.Continuation? 58 | public var cancellable: CancellableTask? 59 | 60 | // MARK: - Functions 61 | 62 | public func receivedLocationManagerEvent(_ event: LocationManagerBridgeEvent) { 63 | switch event { 64 | case .didChangeAuthorization(let status): 65 | stream?.yield(.didChangeAuthorization(status)) 66 | default: 67 | break 68 | } 69 | } 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /Sources/SwiftLocation/Location Managers/LocationManagerBridgeEvent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftLocation 3 | // Async/Await Wrapper for CoreLocation 4 | // 5 | // Copyright (c) 2023 Daniele Margutti (hello@danielemargutti.com). 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | import Foundation 27 | import CoreLocation 28 | 29 | /// This is the list of events who can be received by any task. 30 | public enum LocationManagerBridgeEvent { 31 | 32 | // MARK: - Authorization 33 | 34 | case didChangeLocationEnabled(_ enabled: Bool) 35 | case didChangeAuthorization(_ status: CLAuthorizationStatus) 36 | case didChangeAccuracyAuthorization(_ authorization: CLAccuracyAuthorization) 37 | 38 | // MARK: - Location Monitoring 39 | 40 | case locationUpdatesPaused 41 | case locationUpdatesResumed 42 | case receiveNewLocations(locations: [CLLocation]) 43 | 44 | // MARK: - Region Monitoring 45 | 46 | case didEnterRegion(_ region: CLRegion) 47 | case didExitRegion(_ region: CLRegion) 48 | case didStartMonitoringFor(_ region: CLRegion) 49 | 50 | // MARK: - Failures 51 | 52 | case didFailWithError(_ error: Error) 53 | case monitoringDidFailFor(region: CLRegion?, error: Error) 54 | 55 | // MARK: - Visits Monitoring 56 | 57 | #if !os(watchOS) && !os(tvOS) 58 | case didVisit(visit: CLVisit) 59 | #endif 60 | 61 | // MARK: - Headings 62 | 63 | #if os(iOS) 64 | case didUpdateHeading(_ heading: CLHeading) 65 | #endif 66 | 67 | // MARK: - Beacons 68 | 69 | #if !os(watchOS) && !os(tvOS) 70 | case didRange(beacons: [CLBeacon], constraint: CLBeaconIdentityConstraint) 71 | case didFailRanginFor(constraint: CLBeaconIdentityConstraint, error: Error) 72 | #endif 73 | } 74 | -------------------------------------------------------------------------------- /Sources/SwiftLocation/Async Tasks/LocationServicesEnabled.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftLocation 3 | // Async/Await Wrapper for CoreLocation 4 | // 5 | // Copyright (c) 2023 Daniele Margutti (hello@danielemargutti.com). 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | import Foundation 27 | import CoreLocation 28 | 29 | extension Tasks { 30 | 31 | public final class LocationServicesEnabled: AnyTask { 32 | 33 | // MARK: - Support Structures 34 | 35 | /// Stream produced by the task. 36 | public typealias Stream = AsyncStream 37 | 38 | /// The event produced by the stream. 39 | public enum StreamEvent: CustomStringConvertible, Equatable { 40 | 41 | /// A new change in the location services status has been detected. 42 | case didChangeLocationEnabled(_ enabled: Bool) 43 | 44 | /// Return `true` if location service is enabled. 45 | var isLocationEnabled: Bool { 46 | switch self { 47 | case let .didChangeLocationEnabled(enabled): 48 | return enabled 49 | } 50 | } 51 | 52 | public var description: String { 53 | switch self { 54 | case .didChangeLocationEnabled: 55 | return "didChangeLocationEnabled" 56 | 57 | } 58 | } 59 | 60 | } 61 | 62 | // MARK: - Public Properties 63 | 64 | public let uuid = UUID() 65 | public var stream: Stream.Continuation? 66 | public var cancellable: CancellableTask? 67 | 68 | // MARK: - Functions 69 | 70 | public func receivedLocationManagerEvent(_ event: LocationManagerBridgeEvent) { 71 | switch event { 72 | case .didChangeLocationEnabled(let enabled): 73 | stream?.yield(.didChangeLocationEnabled(enabled)) 74 | default: 75 | break 76 | } 77 | } 78 | 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /Sources/SwiftLocation/Async Tasks/AccuracyAuthorization.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftLocation 3 | // Async/Await Wrapper for CoreLocation 4 | // 5 | // Copyright (c) 2023 Daniele Margutti (hello@danielemargutti.com). 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | import Foundation 27 | import CoreLocation 28 | 29 | extension Tasks { 30 | 31 | public final class AccuracyAuthorization: AnyTask { 32 | 33 | // MARK: - Support Structures 34 | 35 | /// Stream produced by the task. 36 | public typealias Stream = AsyncStream 37 | 38 | /// The event produced by the stream. 39 | public enum StreamEvent: CustomStringConvertible, Equatable { 40 | 41 | /// A new change in accuracy level authorization has been captured. 42 | case didUpdateAccuracyAuthorization(_ accuracyAuthorization: CLAccuracyAuthorization) 43 | 44 | /// Return the accuracy authorization of the event 45 | var accuracyAuthorization: CLAccuracyAuthorization { 46 | switch self { 47 | case let .didUpdateAccuracyAuthorization(accuracyAuthorization): 48 | return accuracyAuthorization 49 | } 50 | } 51 | 52 | public var description: String { 53 | switch self { 54 | case .didUpdateAccuracyAuthorization: 55 | return "didUpdateAccuracyAuthorization" 56 | } 57 | } 58 | 59 | } 60 | 61 | // MARK: - Public Properties 62 | 63 | public let uuid = UUID() 64 | public var stream: Stream.Continuation? 65 | public var cancellable: CancellableTask? 66 | 67 | // MARK: - Functions 68 | 69 | public func receivedLocationManagerEvent(_ event: LocationManagerBridgeEvent) { 70 | switch event { 71 | case .didChangeAccuracyAuthorization(let auth): 72 | stream?.yield(.didUpdateAccuracyAuthorization(auth)) 73 | default: 74 | break 75 | } 76 | } 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /Sources/SwiftLocation/Async Tasks/VisitsMonitoring.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftLocation 3 | // Async/Await Wrapper for CoreLocation 4 | // 5 | // Copyright (c) 2023 Daniele Margutti (hello@danielemargutti.com). 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | import Foundation 27 | import CoreLocation 28 | 29 | #if !os(watchOS) && !os(tvOS) 30 | extension Tasks { 31 | 32 | public final class VisitsMonitoring: AnyTask { 33 | 34 | // MARK: - Support Structures 35 | 36 | /// The event produced by the stream. 37 | public typealias Stream = AsyncStream 38 | 39 | /// The event produced by the stream. 40 | public enum StreamEvent: CustomStringConvertible, Equatable { 41 | 42 | /// A new visit-related event was received. 43 | case didVisit(_ visit: CLVisit) 44 | 45 | /// Receive an error. 46 | case didFailWithError(_ error: Error) 47 | 48 | public var description: String { 49 | switch self { 50 | case .didVisit: 51 | return "didVisit" 52 | case let .didFailWithError(error): 53 | return "didFailWithError: \(error.localizedDescription)" 54 | } 55 | } 56 | 57 | public static func == (lhs: Tasks.VisitsMonitoring.StreamEvent, rhs: Tasks.VisitsMonitoring.StreamEvent) -> Bool { 58 | switch (lhs, rhs) { 59 | case (let .didVisit(v1), let .didVisit(v2)): 60 | return v1 == v2 61 | case (let .didFailWithError(e1), let .didFailWithError(e2)): 62 | return e1.localizedDescription == e2.localizedDescription 63 | default: 64 | return false 65 | } 66 | } 67 | } 68 | 69 | // MARK: - Public Properties 70 | 71 | public let uuid = UUID() 72 | public var stream: Stream.Continuation? 73 | public var cancellable: CancellableTask? 74 | 75 | // MARK: - Functions 76 | 77 | public func receivedLocationManagerEvent(_ event: LocationManagerBridgeEvent) { 78 | switch event { 79 | case let .didVisit(visit): 80 | stream?.yield(.didVisit(visit)) 81 | case let .didFailWithError(error): 82 | stream?.yield(.didFailWithError(error)) 83 | default: 84 | break 85 | } 86 | } 87 | } 88 | 89 | } 90 | #endif 91 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # 💻 Contributing to SwiftLocation 2 | 3 | First of all, thanks for your interest in SwiftLocation. 4 | 5 | There are several ways to contribute to this project. We welcome contributions in all ways. 6 | We have made some contribution guidelines to smoothly incorporate your opinions and code into this project. 7 | 8 | ## 📝 Open Issue 9 | 10 | When you found a bug or having a feature request, search for the issue from the [existing](https://github.com/malcommac/SwiftLocation/issues) and feel free to open the issue after making sure it isn't already reported. 11 | 12 | In order to we understand your issue accurately, please include as much information as possible in the issue template. 13 | The screenshot are also big clue to understand the issue. 14 | 15 | If you know exactly how to fix the bug you report or implement the feature you propose, please pull request instead of an issue. 16 | 17 | ## 🚀 Pull Request 18 | 19 | We are waiting for a pull request to make this project more better with us. 20 | If you want to add a new feature, let's discuss about it first on issue. 21 | 22 | ```bash 23 | $ git clone https://github.com/malcommac/SwiftLocation.git 24 | $ cd SwiftLocation/ 25 | $ open SwiftLocation 26 | ``` 27 | 28 | ### Test 29 | 30 | The test will tells us the validity of your code. 31 | All codes entering the master must pass the all tests. 32 | If you change the code or add new features, you should add tests. 33 | 34 | ### Documentation 35 | 36 | Please write the document using [Xcode markup](https://developer.apple.com/library/archive/documentation/Xcode/Reference/xcode_markup_formatting_ref/) to the code you added. 37 | Documentation template is inserted automatically by using Xcode shortcut **⌥⌘/**. 38 | Our document style is slightly different from the template. The example is below. 39 | ```swift 40 | /// The example class for documentation. 41 | final class Foo { 42 | /// A property value. 43 | let prop: Int 44 | 45 | /// Create a new foo with a param. 46 | /// 47 | /// - Parameters: 48 | /// - param: An Int value for prop. 49 | init(param: Int) { 50 | prop = param 51 | } 52 | 53 | /// Returns a string value concatenating `param1` and `param2`. 54 | /// 55 | /// - Parameters: 56 | /// - param1: An Int value for prefix. 57 | /// - param2: A String value for suffix. 58 | /// 59 | /// - Returns: A string concatenating given params. 60 | func bar(param1: Int, param2: String) -> String { 61 | return "\(param1)" + param2 62 | } 63 | } 64 | ``` 65 | 66 | ## [Developer's Certificate of Origin 1.1](https://elinux.org/Developer_Certificate_Of_Origin) 67 | By making a contribution to this project, I certify that: 68 | 69 | (a) The contribution was created in whole or in part by me and I 70 | have the right to submit it under the open source license 71 | indicated in the file; or 72 | 73 | (b) The contribution is based upon previous work that, to the best 74 | of my knowledge, is covered under an appropriate open source 75 | license and I have the right under that license to submit that 76 | work with modifications, whether created in whole or in part 77 | by me, under the same open source license (unless I am 78 | permitted to submit under a different license), as indicated 79 | in the file; or 80 | 81 | (c) The contribution was provided directly to me by some other 82 | person who certified (a), (b) or (c) and I have not modified 83 | it. 84 | 85 | (d) I understand and agree that this project and the contribution 86 | are public and that a record of the contribution (including all 87 | personal information I submit with it, including my sign-off) is 88 | maintained indefinitely and may be redistributed consistent with 89 | this project or the open source license(s) involved. 90 | -------------------------------------------------------------------------------- /Sources/SwiftLocation/Async Tasks/HeadingMonitoring.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftLocation 3 | // Async/Await Wrapper for CoreLocation 4 | // 5 | // Copyright (c) 2023 Daniele Margutti (hello@danielemargutti.com). 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | import Foundation 27 | import CoreLocation 28 | 29 | #if os(iOS) 30 | extension Tasks { 31 | 32 | public final class HeadingMonitoring: AnyTask { 33 | 34 | // MARK: - Support Structures 35 | 36 | /// The event produced by the stream. 37 | public typealias Stream = AsyncStream 38 | 39 | /// The event produced by the stream. 40 | public enum StreamEvent: CustomStringConvertible, Equatable { 41 | 42 | /// A new heading value has been received. 43 | case didUpdateHeading(_ heading: CLHeading) 44 | 45 | /// An error has occurred. 46 | case didFailWithError(_ error: Error) 47 | 48 | public static func == (lhs: Tasks.HeadingMonitoring.StreamEvent, rhs: Tasks.HeadingMonitoring.StreamEvent) -> Bool { 49 | switch (lhs, rhs) { 50 | case (let .didUpdateHeading(h1), let .didUpdateHeading(h2)): 51 | return h1 == h2 52 | 53 | case (let .didFailWithError(e1), let .didFailWithError(e2)): 54 | return e1.localizedDescription == e2.localizedDescription 55 | 56 | default: 57 | return false 58 | 59 | } 60 | } 61 | 62 | public var description: String { 63 | switch self { 64 | case .didFailWithError: 65 | return "didFailWithError" 66 | 67 | case .didUpdateHeading: 68 | return "didUpdateHeading" 69 | 70 | } 71 | } 72 | } 73 | 74 | // MARK: - Public Properties 75 | 76 | public let uuid = UUID() 77 | public var stream: Stream.Continuation? 78 | public var cancellable: CancellableTask? 79 | 80 | // MARK: - Functions 81 | 82 | public func receivedLocationManagerEvent(_ event: LocationManagerBridgeEvent) { 83 | switch event { 84 | case let .didUpdateHeading(heading): 85 | stream?.yield(.didUpdateHeading(heading)) 86 | case let .didFailWithError(error): 87 | stream?.yield(.didFailWithError(error)) 88 | default: 89 | break 90 | } 91 | } 92 | 93 | } 94 | 95 | } 96 | #endif 97 | -------------------------------------------------------------------------------- /Sources/SwiftLocation/Async Tasks/BeaconMonitoring.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftLocation 3 | // Async/Await Wrapper for CoreLocation 4 | // 5 | // Copyright (c) 2023 Daniele Margutti (hello@danielemargutti.com). 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | import Foundation 27 | import CoreLocation 28 | 29 | #if !os(watchOS) && !os(tvOS) 30 | extension Tasks { 31 | 32 | public final class BeaconMonitoring: AnyTask { 33 | 34 | // MARK: - Support Structures 35 | 36 | /// The event produced by the stream. 37 | public typealias Stream = AsyncStream 38 | 39 | /// The event produced by the stream. 40 | public enum StreamEvent: CustomStringConvertible, Equatable { 41 | case didRange(beacons: [CLBeacon], constraint: CLBeaconIdentityConstraint) 42 | case didFailRanginFor(constraint: CLBeaconIdentityConstraint, error: Error) 43 | 44 | public static func == (lhs: Tasks.BeaconMonitoring.StreamEvent, rhs: Tasks.BeaconMonitoring.StreamEvent) -> Bool { 45 | switch (lhs, rhs) { 46 | case (let .didRange(b1, _), let .didRange(b2, _)): 47 | return b1 == b2 48 | 49 | case (let .didFailRanginFor(c1, _), let .didFailRanginFor(c2, _)): 50 | return c1 == c2 51 | 52 | default: 53 | return false 54 | 55 | } 56 | } 57 | 58 | public var description: String { 59 | switch self { 60 | case .didFailRanginFor: 61 | return "didFailRanginFor" 62 | case .didRange: 63 | return "didRange" 64 | } 65 | } 66 | } 67 | 68 | // MARK: - Public Properties 69 | 70 | public let uuid = UUID() 71 | public var stream: Stream.Continuation? 72 | public var cancellable: CancellableTask? 73 | public private(set) var satisfying: CLBeaconIdentityConstraint 74 | 75 | // MARK: - Initialization 76 | 77 | init(satisfying: CLBeaconIdentityConstraint) { 78 | self.satisfying = satisfying 79 | } 80 | 81 | // MARK: - Functions 82 | 83 | public func receivedLocationManagerEvent(_ event: LocationManagerBridgeEvent) { 84 | switch event { 85 | case let .didRange(beacons, constraint): 86 | stream?.yield(.didRange(beacons: beacons, constraint: constraint)) 87 | case let .didFailRanginFor(constraint, error): 88 | stream?.yield(.didFailRanginFor(constraint: constraint, error: error)) 89 | default: 90 | break 91 | } 92 | } 93 | 94 | } 95 | 96 | } 97 | #endif 98 | -------------------------------------------------------------------------------- /Sources/SwiftLocation/Location Managers/LocationAsyncBridge.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftLocation 3 | // Async/Await Wrapper for CoreLocation 4 | // 5 | // Copyright (c) 2023 Daniele Margutti (hello@danielemargutti.com). 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | import Foundation 27 | import CoreLocation 28 | 29 | /// This bridge is used to link the object which manage the underlying events 30 | /// from `CLLocationManagerDelegate`. 31 | final class LocationAsyncBridge: CancellableTask { 32 | 33 | // MARK: - Private Properties 34 | 35 | private var tasks = [AnyTask]() 36 | weak var location: Location? 37 | 38 | // MARK: - Internal function 39 | 40 | /// Add a new task to the queued operations to bridge. 41 | /// 42 | /// - Parameter task: task to add. 43 | func add(task: AnyTask) { 44 | task.cancellable = self 45 | tasks.append(task) 46 | task.willStart() 47 | } 48 | 49 | /// Cancel the execution of a task. 50 | /// 51 | /// - Parameter task: task to cancel. 52 | func cancel(task: AnyTask) { 53 | cancel(taskUUID: task.uuid) 54 | } 55 | 56 | /// Cancel the execution of a task with a given unique identifier. 57 | /// 58 | /// - Parameter uuid: unique identifier of the task to remove 59 | private func cancel(taskUUID uuid: UUID) { 60 | tasks.removeAll { task in 61 | if task.uuid == uuid { 62 | task.didCancelled() 63 | return true 64 | } else { 65 | return false 66 | } 67 | } 68 | } 69 | 70 | /// Cancel the task of the given class and optional validated condition. 71 | /// 72 | /// - Parameters: 73 | /// - type: type of `AnyTask` conform task to remove. 74 | /// - condition: optional condition to verify in order to cancel. 75 | func cancel(tasksTypes type: AnyTask.Type, condition: ((AnyTask) -> Bool)? = nil) { 76 | let typeToRemove = ObjectIdentifier(type) 77 | tasks.removeAll(where: { 78 | let isCorrectType = ($0.taskType == typeToRemove) 79 | let isConditionValid = (condition == nil ? true : condition!($0)) 80 | let shouldRemove = (isCorrectType && isConditionValid) 81 | 82 | if shouldRemove { 83 | $0.didCancelled() 84 | } 85 | return shouldRemove 86 | }) 87 | } 88 | 89 | /// Dispatch the event to the tasks. 90 | /// 91 | /// - Parameter event: event to dispatch. 92 | func dispatchEvent(_ event: LocationManagerBridgeEvent) { 93 | for task in tasks { 94 | task.receivedLocationManagerEvent(event) 95 | } 96 | 97 | // store cached location 98 | if case .receiveNewLocations(let locations) = event { 99 | location?.lastLocation = locations.last 100 | } 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /Sources/SwiftLocation/Location Managers/LocationManagerProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftLocation 3 | // Async/Await Wrapper for CoreLocation 4 | // 5 | // Copyright (c) 2023 Daniele Margutti (hello@danielemargutti.com). 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | import Foundation 27 | import CoreLocation 28 | 29 | /// The `CLLocationManager` implementation used to provide a mocked version 30 | /// of the system location manager used to write tests. 31 | public protocol LocationManagerProtocol { 32 | 33 | // MARK: - Delegate 34 | 35 | var delegate: CLLocationManagerDelegate? { get set } 36 | 37 | // MARK: - Authorization 38 | 39 | var authorizationStatus: CLAuthorizationStatus { get } 40 | var accuracyAuthorization: CLAccuracyAuthorization { get } 41 | 42 | #if !os(tvOS) 43 | var activityType: CLActivityType { get set } 44 | #endif 45 | 46 | var distanceFilter: CLLocationDistance { get set } 47 | var desiredAccuracy: CLLocationAccuracy { get set } 48 | 49 | #if !os(tvOS) 50 | var allowsBackgroundLocationUpdates: Bool { get set } 51 | #endif 52 | 53 | func locationServicesEnabled() -> Bool 54 | 55 | // MARK: - Location Permissions 56 | 57 | func validatePlistConfigurationOrThrow(permission: LocationPermission) throws 58 | func validatePlistConfigurationForTemporaryAccuracy(purposeKey: String) throws 59 | func requestWhenInUseAuthorization() 60 | 61 | #if !os(tvOS) 62 | func requestAlwaysAuthorization() 63 | #endif 64 | func requestTemporaryFullAccuracyAuthorization(withPurposeKey purposeKey: String, completion: ((Error?) -> Void)?) 65 | 66 | // MARK: - Getting Locations 67 | 68 | #if !os(tvOS) 69 | func startUpdatingLocation() 70 | func stopUpdatingLocation() 71 | #endif 72 | func requestLocation() 73 | 74 | #if !os(watchOS) && !os(tvOS) 75 | // MARK: - Monitoring Regions 76 | 77 | func startMonitoring(for region: CLRegion) 78 | func stopMonitoring(for region: CLRegion) 79 | #endif 80 | 81 | // MARK: - Monitoring Visits 82 | 83 | #if !os(watchOS) && !os(tvOS) 84 | func startMonitoringVisits() 85 | func stopMonitoringVisits() 86 | #endif 87 | 88 | #if !os(watchOS) && !os(tvOS) 89 | // MARK: - Monitoring Significant Location Changes 90 | 91 | func startMonitoringSignificantLocationChanges() 92 | func stopMonitoringSignificantLocationChanges() 93 | #endif 94 | 95 | #if os(iOS) 96 | // MARK: - Getting Heading 97 | 98 | func startUpdatingHeading() 99 | func stopUpdatingHeading() 100 | #endif 101 | 102 | // MARK: - Beacon Ranging 103 | 104 | #if !os(watchOS) && !os(tvOS) 105 | func startRangingBeacons(satisfying constraint: CLBeaconIdentityConstraint) 106 | func stopRangingBeacons(satisfying constraint: CLBeaconIdentityConstraint) 107 | #endif 108 | } 109 | -------------------------------------------------------------------------------- /Sources/SwiftLocation/Async Tasks/SingleUpdateLocation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftLocation 3 | // Async/Await Wrapper for CoreLocation 4 | // 5 | // Copyright (c) 2023 Daniele Margutti (hello@danielemargutti.com). 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | import Foundation 27 | import CoreLocation 28 | 29 | extension Tasks { 30 | 31 | public final class SingleUpdateLocation: AnyTask { 32 | 33 | // MARK: - Support Structures 34 | 35 | public typealias Continuation = CheckedContinuation 36 | 37 | // MARK: - Public Properties 38 | 39 | public let uuid = UUID() 40 | public var cancellable: CancellableTask? 41 | var continuation: Continuation? 42 | 43 | // MARK: - Private Properties 44 | 45 | private var accuracyFilters: AccuracyFilters? 46 | private var timeout: TimeInterval? 47 | private weak var instance: Location? 48 | 49 | // MARK: - Initialization 50 | 51 | init(instance: Location, accuracy: AccuracyFilters?, timeout: TimeInterval?) { 52 | self.instance = instance 53 | self.accuracyFilters = accuracy 54 | self.timeout = timeout 55 | } 56 | 57 | // MARK: - Functions 58 | 59 | func run() async throws -> ContinuousUpdateLocation.StreamEvent { 60 | try await withCheckedThrowingContinuation { continuation in 61 | guard let instance = self.instance else { return } 62 | 63 | self.continuation = continuation 64 | instance.asyncBridge.add(task: self) 65 | instance.locationManager.requestLocation() 66 | } 67 | } 68 | 69 | public func receivedLocationManagerEvent(_ event: LocationManagerBridgeEvent) { 70 | switch event { 71 | case let .receiveNewLocations(locations): 72 | let filteredLocations = AccuracyFilter.filteredLocations(locations, withAccuracyFilters: accuracyFilters) 73 | guard filteredLocations.isEmpty == false else { 74 | return // none of the locations respect passed filters 75 | } 76 | 77 | continuation?.resume(returning: .didUpdateLocations(filteredLocations)) 78 | continuation = nil 79 | cancellable?.cancel(task: self) 80 | case let .didFailWithError(error): 81 | continuation?.resume(returning: .didFailed(error)) 82 | continuation = nil 83 | cancellable?.cancel(task: self) 84 | default: 85 | break 86 | } 87 | } 88 | 89 | public func didCancelled() { 90 | continuation = nil 91 | } 92 | 93 | public func willStart() { 94 | guard let timeout else { 95 | return 96 | } 97 | 98 | DispatchQueue.main.asyncAfter(deadline: .now() + timeout) { [weak self] in 99 | self?.continuation?.resume(throwing: LocationErrors.timeout) 100 | } 101 | } 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /Sources/SwiftLocation/Async Tasks/SignificantLocationMonitoring.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftLocation 3 | // Async/Await Wrapper for CoreLocation 4 | // 5 | // Copyright (c) 2023 Daniele Margutti (hello@danielemargutti.com). 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | import Foundation 27 | import CoreLocation 28 | 29 | extension Tasks { 30 | 31 | public final class SignificantLocationMonitoring: AnyTask { 32 | 33 | // MARK: - Support Structures 34 | 35 | /// The event produced by the stream. 36 | public typealias Stream = AsyncStream 37 | 38 | /// The event produced by the stream. 39 | public enum StreamEvent: CustomStringConvertible, Equatable { 40 | 41 | /// Location changes stream paused. 42 | case didPaused 43 | 44 | /// Location changes stream resumed. 45 | case didResume 46 | 47 | /// New locations received. 48 | case didUpdateLocations(_ locations: [CLLocation]) 49 | 50 | /// An error has occurred. 51 | case didFailWithError(_ error: Error) 52 | 53 | public var description: String { 54 | switch self { 55 | case let .didFailWithError(error): 56 | return "didFailWithError: \(error.localizedDescription)" 57 | case .didPaused: 58 | return "didPaused" 59 | 60 | case .didResume: 61 | return "didResume" 62 | 63 | case .didUpdateLocations: 64 | return "didUpdateLocations" 65 | 66 | } 67 | } 68 | 69 | public static func == (lhs: Tasks.SignificantLocationMonitoring.StreamEvent, rhs: Tasks.SignificantLocationMonitoring.StreamEvent) -> Bool { 70 | switch (lhs, rhs) { 71 | case (let .didFailWithError(e1), let .didFailWithError(e2)): 72 | return e1.localizedDescription == e2.localizedDescription 73 | 74 | case (let .didUpdateLocations(l1), let .didUpdateLocations(l2)): 75 | return l1 == l2 76 | 77 | case (.didPaused, .didPaused): 78 | return true 79 | 80 | case (.didResume, .didResume): 81 | return true 82 | 83 | default: 84 | return false 85 | 86 | } 87 | } 88 | } 89 | 90 | public let uuid = UUID() 91 | public var stream: Stream.Continuation? 92 | public var cancellable: CancellableTask? 93 | 94 | public func receivedLocationManagerEvent(_ event: LocationManagerBridgeEvent) { 95 | switch event { 96 | case let .receiveNewLocations(locations): 97 | stream?.yield(.didUpdateLocations(locations)) 98 | case .locationUpdatesPaused: 99 | stream?.yield(.didPaused) 100 | case .locationUpdatesResumed: 101 | stream?.yield(.didResume) 102 | case let .didFailWithError(error): 103 | stream?.yield(.didFailWithError(error)) 104 | default: 105 | break 106 | } 107 | } 108 | 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /Sources/SwiftLocation/Async Tasks/AccuracyPermission.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftLocation 3 | // Async/Await Wrapper for CoreLocation 4 | // 5 | // Copyright (c) 2023 Daniele Margutti (hello@danielemargutti.com). 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | import Foundation 27 | import CoreLocation 28 | 29 | extension Tasks { 30 | 31 | public final class AccuracyPermission: AnyTask { 32 | 33 | // MARK: - Support Structures 34 | 35 | public typealias Continuation = CheckedContinuation 36 | 37 | // MARK: - Public Properties 38 | 39 | public let uuid = UUID() 40 | public var cancellable: CancellableTask? 41 | var continuation: Continuation? 42 | 43 | // MARK: - Private Properties 44 | 45 | private weak var instance: Location? 46 | 47 | // MARK: - Initialization 48 | 49 | init(instance: Location) { 50 | self.instance = instance 51 | } 52 | 53 | // MARK: - Functions 54 | 55 | public func receivedLocationManagerEvent(_ event: LocationManagerBridgeEvent) { 56 | switch event { 57 | case .didChangeAccuracyAuthorization(let auth): 58 | continuation?.resume(with: .success(auth)) 59 | default: 60 | break 61 | } 62 | } 63 | 64 | func requestTemporaryPermission(purposeKey: String) async throws -> CLAccuracyAuthorization { 65 | try await withCheckedThrowingContinuation { continuation in 66 | guard let instance = self.instance else { return } 67 | 68 | guard instance.locationManager.locationServicesEnabled() else { 69 | continuation.resume(throwing: LocationErrors.locationServicesDisabled) 70 | return 71 | } 72 | 73 | let authorizationStatus = instance.authorizationStatus 74 | guard authorizationStatus != .notDetermined else { 75 | continuation.resume(throwing: LocationErrors.authorizationRequired) 76 | return 77 | } 78 | 79 | let accuracyAuthorization = instance.accuracyAuthorization 80 | guard accuracyAuthorization != .fullAccuracy else { 81 | continuation.resume(with: .success(accuracyAuthorization)) 82 | return 83 | } 84 | 85 | self.continuation = continuation 86 | instance.asyncBridge.add(task: self) 87 | instance.locationManager.requestTemporaryFullAccuracyAuthorization(withPurposeKey: purposeKey) { error in 88 | if let error { 89 | continuation.resume(throwing: error) 90 | return 91 | } 92 | 93 | // If the user chooses reduced accuracy, the didChangeAuthorization delegate method will not called. 94 | if instance.locationManager.accuracyAuthorization == .reducedAccuracy { 95 | let accuracyAuthorization = instance.accuracyAuthorization 96 | instance.asyncBridge.dispatchEvent(.didChangeAccuracyAuthorization(accuracyAuthorization)) 97 | } 98 | } 99 | } 100 | } 101 | 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /Sources/SwiftLocation/Async Tasks/LocatePermission.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftLocation 3 | // Async/Await Wrapper for CoreLocation 4 | // 5 | // Copyright (c) 2023 Daniele Margutti (hello@danielemargutti.com). 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | import Foundation 27 | import CoreLocation 28 | 29 | extension Tasks { 30 | 31 | public final class LocatePermission: AnyTask { 32 | 33 | // MARK: - Support Structures 34 | 35 | public typealias Continuation = CheckedContinuation 36 | 37 | // MARK: - Public Properties 38 | 39 | public let uuid = UUID() 40 | public var cancellable: CancellableTask? 41 | var continuation: Continuation? 42 | 43 | // MARK: - Private Properties 44 | 45 | private weak var instance: Location? 46 | 47 | // MARK: - Initialization 48 | 49 | init(instance: Location) { 50 | self.instance = instance 51 | } 52 | 53 | // MARK: - Functions 54 | 55 | public func receivedLocationManagerEvent(_ event: LocationManagerBridgeEvent) { 56 | switch event { 57 | case .didChangeAuthorization(let authorization): 58 | guard let continuation = continuation else { 59 | cancellable?.cancel(task: self) 60 | return 61 | } 62 | 63 | continuation.resume(returning: authorization) 64 | self.continuation = nil 65 | cancellable?.cancel(task: self) 66 | default: 67 | break 68 | } 69 | } 70 | 71 | func requestWhenInUsePermission() async throws -> CLAuthorizationStatus { 72 | try await withCheckedThrowingContinuation { continuation in 73 | guard let instance = self.instance else { return } 74 | 75 | let isAuthorized = instance.authorizationStatus != .notDetermined 76 | guard !isAuthorized else { 77 | continuation.resume(returning: instance.authorizationStatus) 78 | return 79 | } 80 | 81 | self.continuation = continuation 82 | instance.asyncBridge.add(task: self) 83 | instance.locationManager.requestWhenInUseAuthorization() 84 | } 85 | } 86 | 87 | #if !os(tvOS) 88 | func requestAlwaysPermission() async throws -> CLAuthorizationStatus { 89 | try await withCheckedThrowingContinuation { continuation in 90 | guard let instance = self.instance else { return } 91 | 92 | #if os(macOS) 93 | let isAuthorized = instance.authorizationStatus != .notDetermined 94 | #else 95 | let isAuthorized = instance.authorizationStatus != .notDetermined && instance.authorizationStatus != .authorizedWhenInUse 96 | #endif 97 | guard !isAuthorized else { 98 | continuation.resume(with: .success(instance.authorizationStatus)) 99 | return 100 | } 101 | 102 | self.continuation = continuation 103 | instance.asyncBridge.add(task: self) 104 | instance.locationManager.requestAlwaysAuthorization() 105 | } 106 | } 107 | #endif 108 | 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /Sources/SwiftLocation/Async Tasks/RegionMonitoring.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftLocation 3 | // Async/Await Wrapper for CoreLocation 4 | // 5 | // Copyright (c) 2023 Daniele Margutti (hello@danielemargutti.com). 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | import Foundation 27 | import CoreLocation 28 | 29 | extension Tasks { 30 | 31 | public final class RegionMonitoring: AnyTask { 32 | 33 | // MARK: - Support Structures 34 | 35 | /// The event produced by the stream. 36 | public typealias Stream = AsyncStream 37 | 38 | /// The event produced by the stream. 39 | public enum StreamEvent: CustomStringConvertible, Equatable { 40 | 41 | /// User entered the specified region. 42 | case didEnterTo(region: CLRegion) 43 | 44 | /// User exited from the specified region. 45 | case didExitTo(region: CLRegion) 46 | 47 | /// A new region is being monitored. 48 | case didStartMonitoringFor(region: CLRegion) 49 | 50 | /// Specified region monitoring error occurred. 51 | case monitoringDidFailFor(region: CLRegion?, error: Error) 52 | 53 | public var description: String { 54 | switch self { 55 | case .didEnterTo: 56 | return "didEnterRegion" 57 | case .didExitTo: 58 | return "didExitRegion" 59 | case .didStartMonitoringFor: 60 | return "didStartMonitoring" 61 | case let .monitoringDidFailFor(_, error): 62 | return "monitoringDidFail: \(error.localizedDescription)" 63 | } 64 | } 65 | 66 | public static func == (lhs: Tasks.RegionMonitoring.StreamEvent, rhs: Tasks.RegionMonitoring.StreamEvent) -> Bool { 67 | switch (lhs, rhs) { 68 | case (let .didEnterTo(r1), let .didEnterTo(r2)): 69 | return r1 == r2 70 | case (let .didExitTo(r1), let .didExitTo(r2)): 71 | return r1 == r2 72 | case (let .didStartMonitoringFor(r1), let .didStartMonitoringFor(r2)): 73 | return r1 == r2 74 | case (let .monitoringDidFailFor(r1, e1), let .monitoringDidFailFor(r2, e2)): 75 | return r1 == r2 || e1.localizedDescription == e2.localizedDescription 76 | default: 77 | return false 78 | } 79 | } 80 | 81 | } 82 | 83 | // MARK: - Public Properties 84 | 85 | public let uuid = UUID() 86 | public var stream: Stream.Continuation? 87 | public var cancellable: CancellableTask? 88 | 89 | private weak var instance: Location? 90 | private(set) var region: CLRegion 91 | 92 | // MARK: - Initialization 93 | 94 | init(instance: Location, region: CLRegion) { 95 | self.instance = instance 96 | self.region = region 97 | } 98 | 99 | // MARK: - Functions 100 | 101 | public func receivedLocationManagerEvent(_ event: LocationManagerBridgeEvent) { 102 | switch event { 103 | case let .didStartMonitoringFor(region): 104 | stream?.yield(.didStartMonitoringFor(region: region)) 105 | 106 | case let .didEnterRegion(region): 107 | stream?.yield(.didEnterTo(region: region)) 108 | 109 | case let .didExitRegion(region): 110 | stream?.yield(.didExitTo(region: region)) 111 | 112 | case let .monitoringDidFailFor(region, error): 113 | stream?.yield(.monitoringDidFailFor(region: region, error: error)) 114 | 115 | default: 116 | break 117 | 118 | } 119 | } 120 | 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /Sources/SwiftLocation/Support/LocationDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftLocation 3 | // Async/Await Wrapper for CoreLocation 4 | // 5 | // Copyright (c) 2023 Daniele Margutti (hello@danielemargutti.com). 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | import Foundation 27 | import CoreLocation 28 | 29 | /// This is the class which receive events from the `LocationManagerProtocol` implementation 30 | /// and dispatch to the bridged tasks. 31 | final class LocationDelegate: NSObject, CLLocationManagerDelegate { 32 | 33 | private weak var asyncBridge: LocationAsyncBridge? 34 | 35 | private var locationManager: LocationManagerProtocol { 36 | asyncBridge!.location!.locationManager 37 | } 38 | 39 | init(asyncBridge: LocationAsyncBridge) { 40 | self.asyncBridge = asyncBridge 41 | super.init() 42 | } 43 | 44 | func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { 45 | asyncBridge?.dispatchEvent(.didChangeAuthorization(locationManager.authorizationStatus)) 46 | asyncBridge?.dispatchEvent(.didChangeAccuracyAuthorization(locationManager.accuracyAuthorization)) 47 | asyncBridge?.dispatchEvent(.didChangeLocationEnabled(locationManager.locationServicesEnabled())) 48 | } 49 | 50 | // MARK: - Location Updates 51 | 52 | func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { 53 | asyncBridge?.dispatchEvent(.receiveNewLocations(locations: locations)) 54 | } 55 | 56 | func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { 57 | asyncBridge?.dispatchEvent(.didFailWithError(error)) 58 | } 59 | 60 | // MARK: - Heading Updates 61 | 62 | #if os(iOS) 63 | func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) { 64 | asyncBridge?.dispatchEvent(.didUpdateHeading(newHeading)) 65 | } 66 | #endif 67 | 68 | #if os(iOS) 69 | // MARK: - Pause/Resume 70 | 71 | func locationManagerDidPauseLocationUpdates(_ manager: CLLocationManager) { 72 | asyncBridge?.dispatchEvent(.locationUpdatesPaused) 73 | } 74 | 75 | func locationManagerDidResumeLocationUpdates(_ manager: CLLocationManager) { 76 | asyncBridge?.dispatchEvent(.locationUpdatesResumed) 77 | } 78 | #endif 79 | 80 | // MARK: - Region Monitoring 81 | 82 | #if os(iOS) 83 | func locationManager(_ manager: CLLocationManager, monitoringDidFailFor region: CLRegion?, withError error: Error) { 84 | asyncBridge?.dispatchEvent(.monitoringDidFailFor(region: region, error: error)) 85 | } 86 | 87 | func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) { 88 | asyncBridge?.dispatchEvent(.didEnterRegion(region)) 89 | } 90 | 91 | func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) { 92 | asyncBridge?.dispatchEvent(.didExitRegion(region)) 93 | } 94 | 95 | func locationManager(_ manager: CLLocationManager, didStartMonitoringFor region: CLRegion) { 96 | asyncBridge?.dispatchEvent(.didStartMonitoringFor(region)) 97 | } 98 | #endif 99 | 100 | // MARK: - Visits Monitoring 101 | 102 | #if os(iOS) 103 | func locationManager(_ manager: CLLocationManager, didVisit visit: CLVisit) { 104 | asyncBridge?.dispatchEvent(.didVisit(visit: visit)) 105 | } 106 | #endif 107 | 108 | #if os(iOS) 109 | // MARK: - Beacons Ranging 110 | 111 | func locationManager(_ manager: CLLocationManager, didRange beacons: [CLBeacon], satisfying beaconConstraint: CLBeaconIdentityConstraint) { 112 | asyncBridge?.dispatchEvent(.didRange(beacons: beacons, constraint: beaconConstraint)) 113 | } 114 | 115 | func locationManager(_ manager: CLLocationManager, didFailRangingFor beaconConstraint: CLBeaconIdentityConstraint, error: Error) { 116 | asyncBridge?.dispatchEvent(.didFailRanginFor(constraint: beaconConstraint, error: error)) 117 | } 118 | #endif 119 | 120 | } 121 | -------------------------------------------------------------------------------- /Sources/SwiftLocation/Support/Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftLocation 3 | // Async/Await Wrapper for CoreLocation 4 | // 5 | // Copyright (c) 2023 Daniele Margutti (hello@danielemargutti.com). 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | import Foundation 27 | import CoreLocation 28 | 29 | // MARK: - CoreLocation Extensions 30 | 31 | extension CLLocationManager: LocationManagerProtocol { 32 | 33 | public func locationServicesEnabled() -> Bool { 34 | CLLocationManager.locationServicesEnabled() 35 | } 36 | 37 | /// Evaluate the `Info.plist` file data and throw exceptions in case of misconfiguration. 38 | /// 39 | /// - Parameter permission: permission you would to obtain. 40 | public func validatePlistConfigurationOrThrow(permission: LocationPermission) throws { 41 | switch permission { 42 | #if !os(tvOS) 43 | case .always: 44 | if !Bundle.hasAlwaysAndWhenInUsePermission() { 45 | throw LocationErrors.plistNotConfigured 46 | } 47 | #endif 48 | case .whenInUse: 49 | if !Bundle.hasWhenInUsePermission() { 50 | throw LocationErrors.plistNotConfigured 51 | } 52 | } 53 | } 54 | 55 | public func validatePlistConfigurationForTemporaryAccuracy(purposeKey: String) throws { 56 | guard Bundle.hasTemporaryPermission(purposeKey: purposeKey) else { 57 | throw LocationErrors.plistNotConfigured 58 | } 59 | } 60 | 61 | } 62 | 63 | extension CLAccuracyAuthorization: CustomStringConvertible { 64 | 65 | public var description: String { 66 | switch self { 67 | case .fullAccuracy: 68 | return "fullAccuracy" 69 | case .reducedAccuracy: 70 | return "reducedAccuracy" 71 | @unknown default: 72 | return "Unknown (\(rawValue))" 73 | } 74 | } 75 | 76 | } 77 | 78 | extension CLAuthorizationStatus: CustomStringConvertible { 79 | 80 | public var description: String { 81 | switch self { 82 | case .notDetermined: return "notDetermined" 83 | case .restricted: return "restricted" 84 | case .denied: return "denied" 85 | case .authorizedAlways: return "authorizedAlways" 86 | case .authorizedWhenInUse: return "authorizedWhenInUse" 87 | @unknown default: return "unknown" 88 | } 89 | } 90 | 91 | var canMonitorLocation: Bool { 92 | switch self { 93 | case .authorizedAlways, .authorizedWhenInUse: 94 | return true 95 | default: 96 | return false 97 | } 98 | } 99 | 100 | } 101 | 102 | // MARK: - Foundation Extensions 103 | 104 | extension Bundle { 105 | 106 | private static let alwaysAndWhenInUse = "NSLocationAlwaysAndWhenInUseUsageDescription" 107 | private static let whenInUse = "NSLocationWhenInUseUsageDescription" 108 | private static let temporary = "NSLocationTemporaryUsageDescriptionDictionary" 109 | 110 | static func hasTemporaryPermission(purposeKey: String) -> Bool { 111 | guard let node = Bundle.main.object(forInfoDictionaryKey: temporary) as? NSDictionary, 112 | let value = node.object(forKey: purposeKey) as? String, 113 | value.isEmpty == false else { 114 | return false 115 | } 116 | return true 117 | } 118 | 119 | static func hasWhenInUsePermission() -> Bool { 120 | !(Bundle.main.object(forInfoDictionaryKey: whenInUse) as? String ?? "").isEmpty 121 | } 122 | 123 | static func hasAlwaysAndWhenInUsePermission() -> Bool { 124 | !(Bundle.main.object(forInfoDictionaryKey: alwaysAndWhenInUse) as? String ?? "").isEmpty 125 | } 126 | 127 | } 128 | 129 | extension UserDefaults { 130 | 131 | func set(location:CLLocation?, forKey key: String) { 132 | guard let location else { 133 | removeObject(forKey: key) 134 | return 135 | } 136 | 137 | let locationData = try? NSKeyedArchiver.archivedData(withRootObject: location, requiringSecureCoding: false) 138 | set(locationData, forKey: key) 139 | } 140 | 141 | func location(forKey key: String) -> CLLocation? { 142 | guard let locationData = UserDefaults.standard.data(forKey: key) else { 143 | return nil 144 | } 145 | 146 | do { 147 | return try NSKeyedUnarchiver.unarchivedObject(ofClass: CLLocation.self, from: locationData) 148 | } catch { 149 | return nil 150 | } 151 | } 152 | 153 | } 154 | 155 | -------------------------------------------------------------------------------- /Sources/SwiftLocation/Async Tasks/ContinuousUpdateLocation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftLocation 3 | // Async/Await Wrapper for CoreLocation 4 | // 5 | // Copyright (c) 2023 Daniele Margutti (hello@danielemargutti.com). 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | import Foundation 27 | import CoreLocation 28 | 29 | extension Tasks { 30 | 31 | public final class ContinuousUpdateLocation: AnyTask { 32 | 33 | // MARK: - Support Structures 34 | 35 | /// Stream produced by the task. 36 | public typealias Stream = AsyncStream 37 | 38 | /// The event produced by the stream. 39 | public enum StreamEvent: CustomStringConvertible, Equatable { 40 | 41 | /// A new array of locations has been received. 42 | case didUpdateLocations(_ locations: [CLLocation]) 43 | 44 | /// Something went wrong while reading new locations. 45 | case didFailed(_ error: Error) 46 | 47 | #if os(iOS) 48 | /// Location updates did resume. 49 | case didResume 50 | 51 | /// Location updates did pause. 52 | case didPaused 53 | #endif 54 | 55 | /// Return the location received by the event if it's a location event. 56 | /// In case of multiple events it will return the most recent one. 57 | public var location: CLLocation? { 58 | locations?.max(by: { $0.timestamp < $1.timestamp }) 59 | } 60 | 61 | /// Return the list of locations received if the event is a location update. 62 | public var locations: [CLLocation]? { 63 | guard case .didUpdateLocations(let locations) = self else { 64 | return nil 65 | } 66 | return locations 67 | } 68 | 69 | /// Error received if any. 70 | public var error: Error? { 71 | guard case .didFailed(let e) = self else { 72 | return nil 73 | } 74 | return e 75 | } 76 | 77 | public var description: String { 78 | switch self { 79 | #if os(iOS) 80 | case .didPaused: 81 | return "paused" 82 | case .didResume: 83 | return "resume" 84 | #endif 85 | case let .didFailed(e): 86 | return "error \(e.localizedDescription)" 87 | case let .didUpdateLocations(l): 88 | return "\(l.count) locations" 89 | } 90 | } 91 | 92 | public static func == (lhs: Tasks.ContinuousUpdateLocation.StreamEvent, rhs: Tasks.ContinuousUpdateLocation.StreamEvent) -> Bool { 93 | switch (lhs, rhs) { 94 | case (.didFailed(let e1), .didFailed(let e2)): 95 | return e1.localizedDescription == e2.localizedDescription 96 | #if os(iOS) 97 | case (.didPaused, .didPaused): 98 | return true 99 | case (.didResume, .didResume): 100 | return true 101 | #endif 102 | case (.didUpdateLocations(let l1), .didUpdateLocations(let l2)): 103 | return l1 == l2 104 | default: 105 | return false 106 | } 107 | } 108 | } 109 | 110 | // MARK: - Public Properties 111 | 112 | public let uuid = UUID() 113 | public var stream: Stream.Continuation? 114 | public var cancellable: CancellableTask? 115 | 116 | // MARK: - Private Properties 117 | 118 | private weak var instance: Location? 119 | 120 | // MARK: - Initialization 121 | 122 | init(instance: Location) { 123 | self.instance = instance 124 | } 125 | 126 | // MARK: - Functions 127 | 128 | public func receivedLocationManagerEvent(_ event: LocationManagerBridgeEvent) { 129 | switch event { 130 | #if os(iOS) 131 | case .locationUpdatesPaused: 132 | stream?.yield(.didPaused) 133 | 134 | case .locationUpdatesResumed: 135 | stream?.yield(.didResume) 136 | #endif 137 | 138 | case let .didFailWithError(error): 139 | stream?.yield(.didFailed(error)) 140 | 141 | case let .receiveNewLocations(locations): 142 | stream?.yield(.didUpdateLocations(locations)) 143 | 144 | default: 145 | break 146 | } 147 | } 148 | 149 | public func didCancelled() { 150 | guard let stream = stream else { 151 | return 152 | } 153 | 154 | stream.finish() 155 | self.stream = nil 156 | } 157 | 158 | } 159 | 160 | 161 | } 162 | -------------------------------------------------------------------------------- /Sources/SwiftLocation/Support/SupportModels.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftLocation 3 | // Async/Await Wrapper for CoreLocation 4 | // 5 | // Copyright (c) 2023 Daniele Margutti (hello@danielemargutti.com). 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | import Foundation 27 | import CoreLocation 28 | 29 | // MARK: - Accuracy Filters 30 | 31 | /// A set of accuracy filters. 32 | public typealias AccuracyFilters = [AccuracyFilter] 33 | 34 | extension AccuracyFilters { 35 | 36 | /// Return the highest value of the accuracy level used as filter 37 | /// in both horizontal and vertical direction. 38 | static func highestAccuracyLevel(currentLevel: CLLocationAccuracy = kCLLocationAccuracyReduced, filters: AccuracyFilters?) -> CLLocationAccuracy { 39 | guard let filters else { return currentLevel } 40 | 41 | var value: Double = currentLevel 42 | for filter in filters { 43 | switch filter { 44 | case let .vertical(vValue): 45 | value = Swift.min(value, vValue) 46 | case let .horizontal(hValue): 47 | value = Swift.min(value, hValue) 48 | default: 49 | break 50 | } 51 | } 52 | return value 53 | } 54 | 55 | } 56 | 57 | /// Single Accuracy filter. 58 | public enum AccuracyFilter { 59 | /// Filter for the altitude values, and their estimated uncertainty, measured in meters. 60 | case horizontal(CLLocationAccuracy) 61 | /// Filter for the radius of uncertainty for the location, measured in meters. 62 | case vertical(CLLocationAccuracy) 63 | /// Filter for the accuracy of the speed value, measured in meters per second. 64 | case speed(CLLocationSpeedAccuracy) 65 | /// Filter for the accuracy of the course value, measured in degrees. 66 | case course(CLLocationDirectionAccuracy) 67 | /// Filter using a custom function. 68 | case custom(_ isIncluded: ((CLLocation) -> Bool)) 69 | 70 | // MARK: - Internal Functions 71 | 72 | /// Return a filtered array of the location which match passed filters. 73 | /// 74 | /// - Parameters: 75 | /// - locations: initial array of locations. 76 | /// - filters: filters to validate. 77 | /// - Returns: filtered locations. 78 | static func filteredLocations(_ locations: [CLLocation], withAccuracyFilters filters: AccuracyFilters?) -> [CLLocation] { 79 | guard let filters else { return locations } 80 | return locations.filter { AccuracyFilter.isLocation($0, validForFilters: filters) } 81 | } 82 | 83 | /// Return if location is valid for a given set of accuracy filters. 84 | /// 85 | /// - Parameters: 86 | /// - location: location to validate. 87 | /// - filters: filters used for check. 88 | /// - Returns: `true` if location respect filters passed. 89 | static func isLocation(_ location: CLLocation, validForFilters filters: AccuracyFilters) -> Bool { 90 | let firstInvalidFilter = filters.first { $0.isValidForLocation(location) == false } 91 | let isValid = (firstInvalidFilter == nil) 92 | return isValid 93 | } 94 | 95 | /// Return if location match `self` filter. 96 | /// 97 | /// - Parameter location: location to check. 98 | /// - Returns: `true` if accuracy is valid for given location. 99 | private func isValidForLocation(_ location: CLLocation) -> Bool { 100 | switch self { 101 | case let .horizontal(value): 102 | return location.horizontalAccuracy <= value 103 | case let .vertical(value): 104 | return location.verticalAccuracy <= value 105 | case let .speed(value): 106 | return location.speedAccuracy <= value 107 | case let .course(value): 108 | return location.courseAccuracy <= value 109 | case let .custom(isIncluded): 110 | return isIncluded(location) 111 | } 112 | } 113 | 114 | } 115 | 116 | // MARK: - Location Accuracy 117 | 118 | /// Accuracy level you can set for the location manager. 119 | public enum LocationAccuracy { 120 | /// The highest level of accuracy 121 | /// (available only if precise location authorization is granted). 122 | case best 123 | /// Accurate to within ten meters of the desired target 124 | /// (available only if precise location authorization is granted). 125 | case nearestTenMeters 126 | /// Accurate to within one hundred meters. 127 | /// (available only if precise location authorization is granted). 128 | case hundredMeters 129 | /// Accurate to the nearest kilometer. 130 | /// (available only if precise location authorization is granted). 131 | case kilometer 132 | /// Accurate to the nearest three kilometers. 133 | /// (available only if precise location authorization is granted). 134 | case threeKilometers 135 | /// The highest possible accuracy that uses additional sensor data to facilitate navigation apps. 136 | /// (available only if precise location authorization is granted). 137 | case bestForNavigation 138 | /// Custom precision, may require precise location authorization. 139 | case custom(Double) 140 | 141 | init(level: CLLocationAccuracy) { 142 | switch level { 143 | case kCLLocationAccuracyBest: self = .best 144 | case kCLLocationAccuracyNearestTenMeters: self = .nearestTenMeters 145 | case kCLLocationAccuracyHundredMeters: self = .hundredMeters 146 | case kCLLocationAccuracyKilometer: self = .kilometer 147 | case kCLLocationAccuracyThreeKilometers: self = .threeKilometers 148 | case kCLLocationAccuracyBestForNavigation: self = .bestForNavigation 149 | default: self = .custom(level) 150 | } 151 | } 152 | 153 | internal var level: CLLocationAccuracy { 154 | switch self { 155 | case .best: return kCLLocationAccuracyBest 156 | case .nearestTenMeters: return kCLLocationAccuracyNearestTenMeters 157 | case .hundredMeters: return kCLLocationAccuracyHundredMeters 158 | case .kilometer: return kCLLocationAccuracyKilometer 159 | case .threeKilometers: return kCLLocationAccuracyThreeKilometers 160 | case .bestForNavigation: return kCLLocationAccuracyBestForNavigation 161 | case .custom(let value): return value 162 | } 163 | } 164 | } 165 | 166 | // MARK: - LocationPermission 167 | 168 | public enum LocationPermission { 169 | /// Always authorization, both background and when in use. 170 | #if !os(tvOS) 171 | case always 172 | #endif 173 | /// Only when in use authorization. 174 | case whenInUse 175 | } 176 | -------------------------------------------------------------------------------- /Tests/SwiftLocationTests/MockedLocationManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftLocation 3 | // Async/Await Wrapper for CoreLocation 4 | // 5 | // Copyright (c) 2023 Daniele Margutti (hello@danielemargutti.com). 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | import Foundation 27 | import CoreLocation 28 | @testable import SwiftLocation 29 | 30 | public class MockedLocationManager: LocationManagerProtocol { 31 | 32 | let fakeInstance = CLLocationManager() 33 | public weak var delegate: CLLocationManagerDelegate? 34 | 35 | public var allowsBackgroundLocationUpdates: Bool = false 36 | 37 | public var isLocationServicesEnabled: Bool = true { 38 | didSet { 39 | guard isLocationServicesEnabled != oldValue else { return } 40 | delegate?.locationManagerDidChangeAuthorization?(fakeInstance) 41 | } 42 | } 43 | public var authorizationStatus: CLAuthorizationStatus = .notDetermined { 44 | didSet { 45 | guard authorizationStatus != oldValue else { return } 46 | delegate?.locationManagerDidChangeAuthorization?(fakeInstance) 47 | } 48 | } 49 | 50 | public var accuracyAuthorization: CLAccuracyAuthorization = .reducedAccuracy { 51 | didSet { 52 | guard accuracyAuthorization != oldValue else { return } 53 | delegate?.locationManagerDidChangeAuthorization?(fakeInstance) 54 | } 55 | } 56 | 57 | public var desiredAccuracy: CLLocationAccuracy = 100.0 58 | public var activityType: CLActivityType = .other 59 | public var distanceFilter: CLLocationDistance = kCLDistanceFilterNone 60 | 61 | public var onValidatePlistConfiguration: ((_ permission: LocationPermission) -> Error?) = { _ in 62 | return nil 63 | } 64 | 65 | public var onRequestWhenInUseAuthorization: (() -> CLAuthorizationStatus) = { .notDetermined } 66 | public var onRequestAlwaysAuthorization: (() -> CLAuthorizationStatus) = { .notDetermined } 67 | public var onRequestValidationForTemporaryAccuracy: ((String) -> Error?) = { _ in return nil } 68 | 69 | 70 | public func updateLocations(event: Tasks.ContinuousUpdateLocation.StreamEvent) { 71 | switch event { 72 | case let .didUpdateLocations(locations): 73 | delegate?.locationManager?(fakeInstance, didUpdateLocations: locations) 74 | #if os(iOS) 75 | case .didResume: 76 | delegate?.locationManagerDidResumeLocationUpdates?(fakeInstance) 77 | case .didPaused: 78 | delegate?.locationManagerDidPauseLocationUpdates?(fakeInstance) 79 | #endif 80 | case let .didFailed(error): 81 | delegate?.locationManager?(fakeInstance, didFailWithError: error) 82 | } 83 | } 84 | 85 | #if !os(watchOS) && !os(tvOS) 86 | public func updateSignificantLocation(event: Tasks.SignificantLocationMonitoring.StreamEvent) { 87 | switch event { 88 | case let .didFailWithError(error): 89 | delegate?.locationManager?(fakeInstance, didFailWithError: error) 90 | case .didPaused: 91 | delegate?.locationManagerDidPauseLocationUpdates?(fakeInstance) 92 | case .didResume: 93 | delegate?.locationManagerDidResumeLocationUpdates?(fakeInstance) 94 | case let .didUpdateLocations(locations): 95 | delegate?.locationManager?(fakeInstance, didUpdateLocations: locations) 96 | } 97 | } 98 | #endif 99 | 100 | #if !os(watchOS) && !os(tvOS) 101 | public func updateVisits(event: Tasks.VisitsMonitoring.StreamEvent) { 102 | switch event { 103 | case let .didVisit(visit): 104 | delegate?.locationManager?(fakeInstance, didVisit: visit) 105 | case let .didFailWithError(error): 106 | delegate?.locationManager?(fakeInstance, didFailWithError: error) 107 | } 108 | } 109 | 110 | public func updateRegionMonitoring(event: Tasks.RegionMonitoring.StreamEvent) { 111 | switch event { 112 | case let .didEnterTo(region): 113 | delegate?.locationManager?(fakeInstance, didEnterRegion: region) 114 | 115 | case let .didExitTo(region): 116 | delegate?.locationManager?(fakeInstance, didExitRegion: region) 117 | 118 | case let .didStartMonitoringFor(region): 119 | delegate?.locationManager?(fakeInstance, didStartMonitoringFor: region) 120 | 121 | case let .monitoringDidFailFor(region, error): 122 | delegate?.locationManager?(fakeInstance, monitoringDidFailFor: region, withError: error) 123 | 124 | } 125 | } 126 | #endif 127 | 128 | public func validatePlistConfigurationForTemporaryAccuracy(purposeKey: String) throws { 129 | if let error = onRequestValidationForTemporaryAccuracy(purposeKey) { 130 | throw error 131 | } 132 | } 133 | 134 | public func validatePlistConfigurationOrThrow(permission: LocationPermission) throws { 135 | if let error = onValidatePlistConfiguration(permission) { 136 | throw error 137 | } 138 | } 139 | 140 | public func locationServicesEnabled() -> Bool { 141 | isLocationServicesEnabled 142 | } 143 | 144 | public func requestAlwaysAuthorization() { 145 | self.authorizationStatus = onRequestAlwaysAuthorization() 146 | } 147 | 148 | public func requestWhenInUseAuthorization() { 149 | self.authorizationStatus = onRequestWhenInUseAuthorization() 150 | } 151 | 152 | public func requestTemporaryFullAccuracyAuthorization(withPurposeKey purposeKey: String, completion: ((Error?) -> Void)? = nil) { 153 | self.accuracyAuthorization = .fullAccuracy 154 | completion?(nil) 155 | } 156 | 157 | public func startUpdatingLocation() { 158 | 159 | } 160 | 161 | public func stopUpdatingLocation() { 162 | 163 | } 164 | 165 | public func requestLocation() { 166 | 167 | } 168 | 169 | public func startMonitoring(for region: CLRegion) { 170 | 171 | } 172 | 173 | public func stopMonitoring(for region: CLRegion) { 174 | 175 | } 176 | 177 | public func startMonitoringVisits() { 178 | 179 | } 180 | 181 | public func stopMonitoringVisits() { 182 | 183 | } 184 | 185 | public func startMonitoringSignificantLocationChanges() { 186 | 187 | } 188 | 189 | public func stopMonitoringSignificantLocationChanges() { 190 | 191 | } 192 | 193 | public func startUpdatingHeading() { 194 | 195 | } 196 | 197 | public func stopUpdatingHeading() { 198 | 199 | } 200 | 201 | #if !os(watchOS) && !os(tvOS) 202 | public func startRangingBeacons(satisfying constraint: CLBeaconIdentityConstraint) { 203 | 204 | } 205 | 206 | public func stopRangingBeacons(satisfying constraint: CLBeaconIdentityConstraint) { 207 | 208 | } 209 | #endif 210 | 211 | public init() { 212 | 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | logo-library 5 | 6 |

7 | 8 | [![Platform](https://img.shields.io/badge/Platforms-iOS%20%7C%20macOS%20%7C%20watchOS%20%7C%20tvOS%20-4E4E4E.svg?colorA=28a745)](#installation) 9 | [![Swift](https://img.shields.io/badge/Swift-5.3_5.4_5.5_5.6-orange?style=flat-square)](https://img.shields.io/badge/Swift-5.5_5.6_5.7_5.8_5.9-Orange?style=flat-square) 10 | [![Swift Package Manager](https://img.shields.io/badge/Swift_Package_Manager-compatible-orange?style=flat-square)](https://img.shields.io/badge/Swift_Package_Manager-compatible-orange?style=flat-square) 11 | 12 | **SwiftLocation is a lightweight wrapper around Apple's CoreLocation framework that supports the new Swift Concurrency model.** 13 | 14 | *This means **no more delegate pattern to deal with, nor completion blocks**. 15 | You can manage location requests, region, and beacon monitoring directly using the new async/await syntax.* 16 | 17 | Would you, for example, get the current user location? 18 | *It's just 2 lines code away:* 19 | 20 | ```swift 21 | try await location.requestPermission(.whenInUse) // obtain the permissions 22 | let userLocation = try await location.requestLocation() // get the location 23 | ``` 24 | 25 | # How it works 26 | 27 | SwiftLocation is quite straightforward to use. 28 | Simply create your own `Location` instance and use one of the available methods. 29 | 30 | > [!IMPORTANT] 31 | > Some APIs may not available under some of the supported platforms due to specific hardware constraints. 32 | 33 | - [How it works](#how-it-works) 34 | - [What's new in 6.0](#whats-new-in-60) 35 | - [Service Location Status](#service-location-status) 36 | - [Authorization Status](#authorization-status) 37 | - [Accuracy Authorization Level](#accuracy-authorization-level) 38 | - [Request Location Permission](#request-location-permission) 39 | - [Provide descriptions of how you use location services](#provide-descriptions-of-how-you-use-location-services) 40 | - [Request Temporary Precision Permission](#request-temporary-precision-permission) 41 | - [Continous Location Monitoring](#continous-location-monitoring) 42 | - [Request One-Shot User Location](#request-one-shot-user-location) 43 | - [Visits Monitoring](#visits-monitoring) 44 | - [Significant Location Changes Monitoring](#significant-location-changes-monitoring) 45 | - [Device Heading Monitoring](#device-heading-monitoring) 46 | - [Beacon Ranging](#beacon-ranging) 47 | - [Testing Suite \& Mocked CLLocationManager](#testing-suite---mocked-cllocationmanager) 48 | - [Installation via SPM](#installation-via-spm) 49 | - [Support This Work ❤️](#support-this-work-️) 50 | - [License](#license) 51 | - [Contributing](#contributing) 52 | 53 | ## What's new in 6.0 54 | 55 | The new 6.0 milestone is a completely rewritten version designed to support async/await optimally. We are also focused on supporting all CoreLocation features without creating an overwhelmed package. 56 | All the features are supported by a complete unit tests suite. 57 | 58 | This new version is also distributed only via Swift Package Manager (5.5+) and it's compatible with all the Apple Platforms: iOS 14+, macOS 11+, watchOS 7+, tvOS 14+. 59 | 60 | *The features from version 5.x - geocoding, ip resolve, autocomplete - will be included as separate downloadable modules later in the development process.* 61 | 62 | ## Service Location Status 63 | 64 | Use the `location.locationServicesEnabled` to get the current status of the location services. 65 | In order to monitor changes you can use the `AsyncStream`'s startMonitoringLocationServices()` method: 66 | 67 | ```swift 68 | for await event in await location.startMonitoringLocationServices() { 69 | print("Location Services are \(event.isLocationEnabled ? "enabled" : "disabled")" 70 | // break to interrupt the stream 71 | } 72 | ``` 73 | 74 | You can stop the stream at any moment using `break`; it will call the `stopMonitoringLocationServices()` automatically on used `Location`` instance. 75 | 76 | ## Authorization Status 77 | 78 | You can obtain the current status of the authorization status by using the `location.authorizationStatus` property. 79 | If you need to monitor changes to this value you can use the `AsyncStream` offered by `startMonitoringAuthorization()` method: 80 | 81 | ```swift 82 | for await event in await location.startMonitoringAuthorization() { 83 | print("Authorization status did change: \(event.authorizationStatus)") 84 | // break to interrupt the stream 85 | } 86 | ``` 87 | 88 | ## Accuracy Authorization Level 89 | 90 | The `location.accuracyAuthorization` offers a one shot value of the current precision level offered by your application. 91 | When you need to monitor changes you can use the `AsyncStream` offered by `startMonitoringAccuracyAuthorization()`: 92 | 93 | ```swift 94 | for await event in await location.startMonitoringAccuracyAuthorization() { 95 | print("Accuracy authorization did change: \(event.accuracyAuthorization.description)") 96 | // break to interrupt the stream 97 | } 98 | ``` 99 | 100 | ## Request Location Permission 101 | 102 | Also the request location permission is managed via async await. You can use the `requestPermission()` method once you have properly configured your `Info.plist` file: 103 | 104 | ```swift 105 | // return obtained CLAuthorizationStatus level 106 | let obtaninedStatus = try await location.requestPermission(.whenInUse) 107 | ``` 108 | 109 | ### Provide descriptions of how you use location services 110 | 111 | The first time you make an authorization request, the system displays an alert asking the person to grant or deny the request. The alert includes a usage description string that explains why you want access to location data. 112 | 113 | You provide this string in your app’s Info.plist file and use it to inform people about how your app uses location data. 114 | 115 | Core Location supports different usage strings for each access level. You must include a usage description string for When in Use access. If your app supports Always access, provide an additional string explaining why you want the elevated privileges. The following table lists the keys to include in your Info.plist and when to include them. 116 | 117 | | Usage key | Required when: | 118 | |-----------------------------------------------------------|----------------------------------------------------------------------------------| 119 | | `NSLocationWhenInUseUsageDescription` | The app requests When in Use or Always authorization. | 120 | | `NSLocationAlwaysAndWhenInUseUsageDescription` | The app requests Always authorization. | 121 | | `NSLocationTemporaryUsageDescriptionDictionary` | Used when you want to temporary extend the precision of your authorization level | 122 | | | | 123 | 124 | ## Request Temporary Precision Permission 125 | 126 | If the App does not require an exact location for all of its features, but it is required to have accurate one only for specific features (i.e during checkout, booking service, etc) — then App may ask for temporary accuracy level for that session only using the `requestTemporaryPrecisionAuthorization(purpose:)` method: 127 | 128 | ```swift 129 | // return CLAccuracyAuthorization value 130 | let status = try await location.requestTemporaryPrecisionAuthorization(purpose: "booking") 131 | ``` 132 | 133 | ## Continous Location Monitoring 134 | 135 | If you need to continous monitoring new locations from user's device you can use the `AsyncStream` offered by `startMonitoringLocations()`: 136 | 137 | ```swift 138 | for await event in try await location.startMonitoringLocations() { 139 | switch event { 140 | case .didPaused: 141 | // location updates paused 142 | case .didResume: 143 | // location updates resumed 144 | case let .didUpdateLocations(locations): 145 | // new locations received 146 | case let .didFailed(error): 147 | // an error has occurred 148 | } 149 | // break to stop the stream 150 | } 151 | ``` 152 | 153 | ## Request One-Shot User Location 154 | 155 | Sometimes you may need to get the user location as single value. The async's `requestLocation(accuracy:timeout:)` method was created to return an optionally filtered location within a valid time interval: 156 | 157 | ```swift 158 | // Simple implementation to get the last user location 159 | let location = try await location.requestLocation() 160 | 161 | // Optionally you can return a value only if satisfy one or more constraints 162 | let location = try await location.requestLocation(accuracy: [ 163 | .horizontal(100) // has an horizontal accuracy of 100 meters or lower 164 | ], timeout: 8) // wait for response for a max of 8 seconds 165 | ``` 166 | 167 | Filters include horizontal/vertical, speed, course accuracy and it offer the opportunity to set a custom filter functions as callback. 168 | 169 | ## Visits Monitoring 170 | 171 | Visits monitoring allows you to observe places that the user has been. 172 | Visit objects are created by the system and delivered by the CLLocationManager. 173 | The visit includes the location where the visit occurred and information about the arrival and departure times as relevant. 174 | 175 | To monitor visits you can use the `AsyncStream`'s `startMonitoringVisits()` method: 176 | 177 | ```swift 178 | for await event in await location.startMonitoringVisits() { 179 | switch event { 180 | case let .didVisit(place): 181 | // a new CLVisit object has been received. 182 | case let .didFailWithError(error): 183 | // an error has occurred 184 | } 185 | } 186 | ``` 187 | 188 | ## Significant Location Changes Monitoring 189 | 190 | The `AsyncStream`'s `startMonitoringSignificantLocationChanges()` method starts the generation of updates based on significant location changes. 191 | 192 | ```swift 193 | for await event in await self.location.startMonitoringSignificantLocationChanges() { 194 | switch event { 195 | case .didPaused: 196 | // stream paused 197 | case .didResume: 198 | // stream resumed 199 | case .didUpdateLocations(locations): 200 | // new locations received 201 | case let .didFailWithError(error): 202 | // an error has occured 203 | } 204 | // break to stop the stream 205 | } 206 | ``` 207 | 208 | ## Device Heading Monitoring 209 | 210 | To get updates about the current device's heading use the `AsyncStream` offered by `startUpdatingHeading()` method: 211 | 212 | ```swift 213 | for await event in await self.location.startUpdatingHeading() { 214 | // a new heading value has been generated 215 | } 216 | ``` 217 | 218 | ## Beacon Ranging 219 | 220 | Beacon ranging is offered by the `AsyncStream`'s `startRangingBeacons()` method: 221 | 222 | ```swift 223 | let constraint: CLBeaconIdentityConstraint = ... 224 | for await event in await location.startRangingBeacons(satisfying: constraint) { 225 | // a new event has been generated 226 | } 227 | ``` 228 | 229 | # Testing Suite & Mocked CLLocationManager 230 | 231 | SwiftLocation is distribuited with an extensive unit testing suite you can found into the `SwiftLocationTests` folder. 232 | Inside the suite you will also found the `MockedLocationManager.swift` file which is a `CLLocationManager` mock class you can use to provide the testing suite for your application. By configuring and extending this file you will be able to mock results of location requests and monitoring directly in your host app. 233 | 234 | 235 | # Installation via SPM 236 | 237 | SwiftLocation is offered via Swift Package Manager. 238 | Add it as a dependency in a Swift Package, and add it to your `Package.swift`: 239 | 240 | ```swift 241 | dependencies: [ 242 | .package(url: "https://github.com/malcommac/SwiftLocation.git", from: "6.0.0") 243 | ] 244 | ``` 245 | # Support This Work ❤️ 246 | 247 | If you love this library and wanna encourage further development **consider becoming a sponsor of my work** via [Github Sponsorship](https://github.com/sponsors/malcommac). 248 | 249 | # License 250 | 251 | This package was created and maintaned by [Daniele Margutti](https://github.com/malcommac). 252 | 253 | - [LinkedIn Profile](https://www.linkedin.com/in/danielemargutti/) 254 | - [X/Twitter](http://twitter.com/danielemargutti) 255 | - [Website](https://www.danielemargutti.com) 256 | 257 | It was distribuited using [MIT License](https://github.com/malcommac/SwiftLocation/blob/master/LICENSE.md). 258 | 259 | # Contributing 260 | 261 | - If you need help or you'd like to ask a general question, open an issue. 262 | - If you found a bug, open an issue. 263 | - If you have a feature request, open an issue. 264 | - If you want to contribute, submit a pull request. 265 | 266 | Read the [CONTRIBUTING](CONTRIBUTING.md) file for more informations. 267 | -------------------------------------------------------------------------------- /Sources/SwiftLocation/Location.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftLocation 3 | // Async/Await Wrapper for CoreLocation 4 | // 5 | // Copyright (c) 2023 Daniele Margutti (hello@danielemargutti.com). 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | import Foundation 27 | import CoreLocation 28 | 29 | /// Instantiate this class to query and setup the Location Services and all the function 30 | /// of the library itself. 31 | public final class Location { 32 | 33 | // MARK: - Private Properties 34 | 35 | /// Underlying location manager implementation. 36 | private(set) var locationManager: LocationManagerProtocol 37 | 38 | /// Bridge for async/await communication via tasks. 39 | private(set) var asyncBridge = LocationAsyncBridge() 40 | 41 | /// The delegate which receive events from the underlying `locationManager` implementation 42 | /// and dispatch them to the `asyncBridge` through the final output function. 43 | private(set) var locationDelegate: LocationDelegate 44 | 45 | /// Cache used to store some bits of the data retrived by the underlying core location service. 46 | private let cache = UserDefaults(suiteName: "com.swiftlocation.cache") 47 | private let locationCacheKey = "lastLocation" 48 | 49 | // MARK: - Public Properties 50 | 51 | /// The last received location from underlying Location Manager service. 52 | /// This is persistent between sesssions and store the latest result with no 53 | /// filters or logic behind. 54 | public internal(set) var lastLocation: CLLocation? { 55 | get { 56 | cache?.location(forKey: locationCacheKey) 57 | } 58 | set { 59 | cache?.set(location: newValue, forKey: locationCacheKey) 60 | } 61 | } 62 | 63 | /// Indicate whether location services are enabled on the device. 64 | /// 65 | /// NOTE: 66 | /// This is an async function in order to prevent warning from the compiler. 67 | public var locationServicesEnabled: Bool { 68 | get async { 69 | await Task.detached { 70 | self.locationManager.locationServicesEnabled() 71 | }.value 72 | } 73 | } 74 | 75 | /// The status of your app’s authorization to provide parental controls. 76 | public var authorizationStatus: CLAuthorizationStatus { 77 | locationManager.authorizationStatus 78 | } 79 | 80 | /// Indicates the level of location accuracy the app has permission to use. 81 | public var accuracyAuthorization: CLAccuracyAuthorization { 82 | locationManager.accuracyAuthorization 83 | } 84 | 85 | /// Indicates the accuracy of the location data that your app wants to receive. 86 | public var accuracy: LocationAccuracy { 87 | get { .init(level: locationManager.desiredAccuracy) } 88 | set { locationManager.desiredAccuracy = newValue.level } 89 | } 90 | 91 | #if !os(tvOS) 92 | /// The type of activity the app expects the user to typically perform while in the app’s location session. 93 | /// By default is set to `CLActivityType.other`. 94 | public var activityType: CLActivityType { 95 | get { locationManager.activityType } 96 | set { locationManager.activityType = newValue } 97 | } 98 | #endif 99 | 100 | /// The minimum distance in meters the device must move horizontally before an update event is generated. 101 | /// By defualt is set to `kCLDistanceFilterNone`. 102 | /// 103 | /// NOTE: 104 | /// Use this property only in conjunction with the Standard location services and not with the Significant-change or Visits services. 105 | public var distanceFilter: CLLocationDistance { 106 | get { locationManager.distanceFilter } 107 | set { locationManager.distanceFilter = newValue } 108 | } 109 | 110 | /// Indicates whether the app receives location updates when running in the background. 111 | /// By default is `false`. 112 | /// 113 | /// NOTE: 114 | /// You must set the `UIBackgroundModes` in your `Info.plist` file in order to support this mode. 115 | /// 116 | /// When the value of this property is true and you start location updates while the app is in the foreground, 117 | /// Core Location configures the system to keep the app running to receive continuous background location updates, 118 | /// and arranges to show the background location indicator (blue bar or pill) if needed. 119 | /// Updates continue even if the app subsequently enters the background. 120 | #if !os(tvOS) 121 | public var allowsBackgroundLocationUpdates: Bool { 122 | get { locationManager.allowsBackgroundLocationUpdates } 123 | set { locationManager.allowsBackgroundLocationUpdates = newValue } 124 | } 125 | #endif 126 | 127 | // MARK: - Initialization 128 | 129 | #if !os(tvOS) 130 | /// Initialize a new SwiftLocation instance to work with the Core Location service. 131 | /// 132 | /// - Parameter locationManager: underlying service. By default the device's CLLocationManager instance is used 133 | /// but you can provide your own. 134 | /// - Parameter allowsBackgroundLocationUpdates: Use this property to enable and disable background updates programmatically. 135 | /// By default is `false`. Read the documentation to configure the environment correctly. 136 | public init(locationManager: LocationManagerProtocol = CLLocationManager(), 137 | allowsBackgroundLocationUpdates: Bool = false) { 138 | self.locationDelegate = LocationDelegate(asyncBridge: self.asyncBridge) 139 | self.locationManager = locationManager 140 | self.locationManager.delegate = locationDelegate 141 | self.locationManager.allowsBackgroundLocationUpdates = allowsBackgroundLocationUpdates 142 | self.asyncBridge.location = self 143 | } 144 | #else 145 | /// Initialize a new SwiftLocation instance to work with the Core Location service. 146 | /// 147 | /// - Parameter locationManager: underlying service. By default the device's CLLocationManager instance is used 148 | /// but you can provide your own. 149 | public init(locationManager: LocationManagerProtocol = CLLocationManager()) { 150 | self.locationDelegate = LocationDelegate(asyncBridge: self.asyncBridge) 151 | self.locationManager = locationManager 152 | self.locationManager.delegate = locationDelegate 153 | self.asyncBridge.location = self 154 | } 155 | #endif 156 | 157 | // MARK: - Monitor Location Services Enabled 158 | 159 | /// Initiate a new async stream to monitor the status of the location services. 160 | /// - Returns: observable async stream. 161 | public func startMonitoringLocationServices() async -> Tasks.LocationServicesEnabled.Stream { 162 | let task = Tasks.LocationServicesEnabled() 163 | return Tasks.LocationServicesEnabled.Stream { stream in 164 | task.stream = stream 165 | asyncBridge.add(task: task) 166 | stream.onTermination = { @Sendable _ in 167 | self.stopMonitoringLocationServices() 168 | } 169 | } 170 | } 171 | 172 | /// Stop observing the location services status updates. 173 | public func stopMonitoringLocationServices() { 174 | asyncBridge.cancel(tasksTypes: Tasks.LocationServicesEnabled.self) 175 | } 176 | 177 | // MARK: - Monitor Authorization Status 178 | 179 | /// Monitor updates about the authorization status. 180 | /// 181 | /// - Returns: stream of authorization statuses. 182 | public func startMonitoringAuthorization() async -> Tasks.Authorization.Stream { 183 | let task = Tasks.Authorization() 184 | return Tasks.Authorization.Stream { stream in 185 | task.stream = stream 186 | asyncBridge.add(task: task) 187 | stream.onTermination = { @Sendable _ in 188 | self.stopMonitoringAuthorization() 189 | } 190 | } 191 | } 192 | 193 | /// Stop monitoring changes of authorization status by stopping all running streams. 194 | public func stopMonitoringAuthorization() { 195 | asyncBridge.cancel(tasksTypes: Tasks.Authorization.self) 196 | } 197 | 198 | // MARK: - Monitor Accuracy Authorization 199 | 200 | /// Monitor accuracy authorization level. 201 | /// 202 | /// - Returns: a stream of statuses. 203 | public func startMonitoringAccuracyAuthorization() async -> Tasks.AccuracyAuthorization.Stream { 204 | let task = Tasks.AccuracyAuthorization() 205 | return Tasks.AccuracyAuthorization.Stream { stream in 206 | task.stream = stream 207 | asyncBridge.add(task: task) 208 | stream.onTermination = { @Sendable _ in 209 | self.stopMonitoringAccuracyAuthorization() 210 | } 211 | } 212 | } 213 | 214 | /// Stop monitoring accuracy authorization status by stopping all running streams. 215 | public func stopMonitoringAccuracyAuthorization() { 216 | asyncBridge.cancel(tasksTypes: Tasks.AccuracyAuthorization.self) 217 | } 218 | 219 | // MARK: - Request Permission for Location 220 | 221 | /// Request to monitor location changes. 222 | /// 223 | /// - Parameter permission: type of permission you would to require. 224 | /// - Returns: obtained status. 225 | @discardableResult 226 | public func requestPermission(_ permission: LocationPermission) async throws -> CLAuthorizationStatus { 227 | try locationManager.validatePlistConfigurationOrThrow(permission: permission) 228 | 229 | switch permission { 230 | case .whenInUse: 231 | return try await requestWhenInUsePermission() 232 | #if !os(tvOS) 233 | case .always: 234 | #if APPCLIP 235 | return try await requestWhenInUsePermission() 236 | #else 237 | return try await requestAlwaysPermission() 238 | #endif 239 | #endif 240 | } 241 | } 242 | 243 | /// Temporary request the authorization to get precise location of the user. 244 | /// 245 | /// - Parameter key: purpose key used to prompt the user. Must be defined into the `Info.plist`. 246 | /// - Returns: obtained status. 247 | @discardableResult 248 | public func requestTemporaryPrecisionAuthorization(purpose key: String) async throws -> CLAccuracyAuthorization { 249 | try locationManager.validatePlistConfigurationForTemporaryAccuracy(purposeKey: key) 250 | return try await requestTemporaryPrecisionPermission(purposeKey: key) 251 | } 252 | 253 | // MARK: - Monitor Location Updates 254 | 255 | #if !os(tvOS) 256 | /// Start receiving changes of the locations with a stream. 257 | /// 258 | /// - Returns: events received from the location manager. 259 | public func startMonitoringLocations() async throws -> Tasks.ContinuousUpdateLocation.Stream { 260 | guard locationManager.authorizationStatus != .notDetermined else { 261 | throw LocationErrors.authorizationRequired 262 | } 263 | 264 | guard locationManager.authorizationStatus.canMonitorLocation else { 265 | throw LocationErrors.notAuthorized 266 | } 267 | 268 | let task = Tasks.ContinuousUpdateLocation(instance: self) 269 | return Tasks.ContinuousUpdateLocation.Stream { stream in 270 | task.stream = stream 271 | asyncBridge.add(task: task) 272 | 273 | locationManager.startUpdatingLocation() 274 | stream.onTermination = { @Sendable _ in 275 | self.asyncBridge.cancel(task: task) 276 | } 277 | } 278 | } 279 | 280 | /// Stop updating location updates streams. 281 | public func stopUpdatingLocation() { 282 | locationManager.stopUpdatingLocation() 283 | asyncBridge.cancel(tasksTypes: Tasks.ContinuousUpdateLocation.self) 284 | } 285 | #endif 286 | 287 | // MARK: - Get Location 288 | 289 | /// Request a one-shot location from the underlying core location service. 290 | /// 291 | /// - Parameters: 292 | /// - filters: optional filters to receive only the first event which satisfy filters. 293 | /// - timeout: timeout interval for the request. 294 | /// - Returns: event received. 295 | public func requestLocation(accuracy filters: AccuracyFilters? = nil, 296 | timeout: TimeInterval? = nil) async throws -> Tasks.ContinuousUpdateLocation.StreamEvent { 297 | 298 | // Setup the desidered accuracy based upon the highest resolution. 299 | locationManager.desiredAccuracy = AccuracyFilters.highestAccuracyLevel(currentLevel: locationManager.desiredAccuracy, filters: filters) 300 | let task = Tasks.SingleUpdateLocation(instance: self, accuracy: filters, timeout: timeout) 301 | return try await withTaskCancellationHandler { 302 | try await task.run() 303 | } onCancel: { 304 | asyncBridge.cancel(task: task) 305 | } 306 | } 307 | 308 | #if !os(watchOS) && !os(tvOS) 309 | // MARK: - Monitor Regions 310 | 311 | /// Starts the monitoring a region and receive stream of events from it. 312 | /// 313 | /// - Parameter region: region to monitor. 314 | /// - Returns: stream of events. 315 | public func startMonitoring(region: CLRegion) async throws -> Tasks.RegionMonitoring.Stream { 316 | let task = Tasks.RegionMonitoring(instance: self, region: region) 317 | return Tasks.RegionMonitoring.Stream { stream in 318 | task.stream = stream 319 | asyncBridge.add(task: task) 320 | locationManager.startMonitoring(for: region) 321 | stream.onTermination = { @Sendable _ in 322 | self.asyncBridge.cancel(task: task) 323 | } 324 | } 325 | } 326 | 327 | /// Stop monitoring a region. 328 | /// 329 | /// - Parameter region: region. 330 | public func stopMonitoring(region: CLRegion) { 331 | asyncBridge.cancel(tasksTypes: Tasks.RegionMonitoring.self) { 332 | ($0 as! Tasks.RegionMonitoring).region == region 333 | } 334 | } 335 | #endif 336 | 337 | // MARK: - Monitor Visits Updates 338 | 339 | #if !os(watchOS) && !os(tvOS) 340 | /// Starts monitoring visits to locations. 341 | /// 342 | /// - Returns: stream of events for visits. 343 | public func startMonitoringVisits() async -> Tasks.VisitsMonitoring.Stream { 344 | let task = Tasks.VisitsMonitoring() 345 | return Tasks.VisitsMonitoring.Stream { stream in 346 | task.stream = stream 347 | asyncBridge.add(task: task) 348 | locationManager.startMonitoringVisits() 349 | stream.onTermination = { @Sendable _ in 350 | self.stopMonitoringVisits() 351 | } 352 | } 353 | } 354 | 355 | /// Stop monitoring visits updates. 356 | public func stopMonitoringVisits() { 357 | asyncBridge.cancel(tasksTypes: Tasks.VisitsMonitoring.self) 358 | locationManager.stopMonitoringVisits() 359 | } 360 | #endif 361 | 362 | #if !os(watchOS) && !os(tvOS) 363 | // MARK: - Monitor Significant Locations 364 | 365 | /// Starts monitoring significant location changes. 366 | /// 367 | /// - Returns: stream of events of location changes. 368 | public func startMonitoringSignificantLocationChanges() async -> Tasks.SignificantLocationMonitoring.Stream { 369 | let task = Tasks.SignificantLocationMonitoring() 370 | return Tasks.SignificantLocationMonitoring.Stream { stream in 371 | task.stream = stream 372 | asyncBridge.add(task: task) 373 | locationManager.startMonitoringSignificantLocationChanges() 374 | stream.onTermination = { @Sendable _ in 375 | self.stopMonitoringSignificantLocationChanges() 376 | } 377 | } 378 | } 379 | 380 | /// Stops monitoring location changes. 381 | public func stopMonitoringSignificantLocationChanges() { 382 | locationManager.stopMonitoringSignificantLocationChanges() 383 | asyncBridge.cancel(tasksTypes: Tasks.SignificantLocationMonitoring.self) 384 | } 385 | #endif 386 | 387 | #if os(iOS) 388 | // MARK: - Monitor Device Heading Updates 389 | 390 | /// Starts monitoring heading changes. 391 | /// 392 | /// - Returns: stream of events for heading 393 | public func startUpdatingHeading() async -> Tasks.HeadingMonitoring.Stream { 394 | let task = Tasks.HeadingMonitoring() 395 | return Tasks.HeadingMonitoring.Stream { stream in 396 | task.stream = stream 397 | asyncBridge.add(task: task) 398 | locationManager.startUpdatingHeading() 399 | stream.onTermination = { @Sendable _ in 400 | self.stopUpdatingHeading() 401 | } 402 | } 403 | } 404 | 405 | /// Stops monitoring device heading changes. 406 | public func stopUpdatingHeading() { 407 | locationManager.stopUpdatingHeading() 408 | asyncBridge.cancel(tasksTypes: Tasks.HeadingMonitoring.self) 409 | } 410 | #endif 411 | 412 | // MARK: - Monitor Beacons Ranging 413 | 414 | /// Starts the delivery of notifications for the specified beacon region. 415 | /// 416 | /// - Parameter satisfying: A `CLBeaconIdentityConstraint` constraint. 417 | /// - Returns: stream of events related to passed constraint. 418 | #if !os(watchOS) && !os(tvOS) 419 | public func startRangingBeacons(satisfying: CLBeaconIdentityConstraint) async -> Tasks.BeaconMonitoring.Stream { 420 | let task = Tasks.BeaconMonitoring(satisfying: satisfying) 421 | return Tasks.BeaconMonitoring.Stream { stream in 422 | task.stream = stream 423 | asyncBridge.add(task: task) 424 | locationManager.startRangingBeacons(satisfying: satisfying) 425 | stream.onTermination = { @Sendable _ in 426 | self.stopRangingBeacons(satisfying: satisfying) 427 | } 428 | } 429 | } 430 | 431 | /// Stops monitoring beacon ranging for passed constraint. 432 | /// 433 | /// - Parameter satisfying: A `CLBeaconIdentityConstraint` constraint. 434 | public func stopRangingBeacons(satisfying: CLBeaconIdentityConstraint) { 435 | asyncBridge.cancel(tasksTypes: Tasks.BeaconMonitoring.self) { 436 | ($0 as! Tasks.BeaconMonitoring).satisfying == satisfying 437 | } 438 | locationManager.stopRangingBeacons(satisfying: satisfying) 439 | } 440 | #endif 441 | 442 | // MARK: - Private Functions 443 | 444 | /// Request temporary increase of the precision. 445 | /// 446 | /// - Parameter purposeKey: purpose key. 447 | /// - Returns: authorization obtained. 448 | private func requestTemporaryPrecisionPermission(purposeKey: String) async throws -> CLAccuracyAuthorization { 449 | let task = Tasks.AccuracyPermission(instance: self) 450 | return try await withTaskCancellationHandler { 451 | try await task.requestTemporaryPermission(purposeKey: purposeKey) 452 | } onCancel: { 453 | asyncBridge.cancel(task: task) 454 | } 455 | } 456 | 457 | /// Request authorization to get location when app is in use. 458 | /// 459 | /// - Returns: authorization obtained. 460 | private func requestWhenInUsePermission() async throws -> CLAuthorizationStatus { 461 | let task = Tasks.LocatePermission(instance: self) 462 | return try await withTaskCancellationHandler { 463 | try await task.requestWhenInUsePermission() 464 | } onCancel: { 465 | asyncBridge.cancel(task: task) 466 | } 467 | } 468 | 469 | #if !os(tvOS) 470 | /// Request authorization to get location both in foreground and background. 471 | /// 472 | /// - Returns: authorization obtained. 473 | private func requestAlwaysPermission() async throws -> CLAuthorizationStatus { 474 | let task = Tasks.LocatePermission(instance: self) 475 | return try await withTaskCancellationHandler { 476 | try await task.requestAlwaysPermission() 477 | } onCancel: { 478 | asyncBridge.cancel(task: task) 479 | } 480 | } 481 | #endif 482 | 483 | } 484 | -------------------------------------------------------------------------------- /Tests/SwiftLocationTests/SwiftLocationTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftLocation 3 | // Async/Await Wrapper for CoreLocation 4 | // 5 | // Copyright (c) 2023 Daniele Margutti (hello@danielemargutti.com). 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | import XCTest 27 | import CoreLocation 28 | @testable import SwiftLocation 29 | 30 | final class SwiftLocationTests: XCTestCase { 31 | 32 | private var mockLocationManager: MockedLocationManager! 33 | private var location: Location! 34 | 35 | override func setUp() { 36 | super.setUp() 37 | 38 | self.mockLocationManager = MockedLocationManager() 39 | self.location = Location(locationManager: mockLocationManager) 40 | } 41 | 42 | // MARK: - Tests 43 | 44 | /// Tests the location services enabled changes. 45 | func testMonitoringLocationServicesEnabled() async throws { 46 | let expectedValues = simulateLocationServicesChanges() 47 | var idx = 0 48 | for await event in await self.location.startMonitoringLocationServices() { 49 | print("Location services enabled did change: \(event.isLocationEnabled ? "enabled" : "disabled")") 50 | XCTAssertEqual(expectedValues[idx], event.isLocationEnabled, "Failed to get correct values from location services enabled") 51 | idx += 1 52 | if idx == expectedValues.count { 53 | break // terminate the stream will also automatically remove the monitoring. 54 | } 55 | } 56 | } 57 | 58 | /// Test authorization status changes. 59 | func testMonitoringAuthorizationStatus() async throws { 60 | let expectedValues = simulateAuthorizationStatusChanges() 61 | var idx = 0 62 | for await event in await self.location.startMonitoringAuthorization() { 63 | print("Authorization status did change: \(event.authorizationStatus.description)") 64 | XCTAssertEqual(expectedValues[idx], event.authorizationStatus, "Failed to get correct values from authorization status") 65 | idx += 1 66 | if idx == expectedValues.count { 67 | break 68 | } 69 | } 70 | } 71 | 72 | /// Test accuracy authorization changes. 73 | func testMonitoringAccuracyAuthorization() async throws { 74 | let expectedValues = simulateAccuracyAuthorizationChanges() 75 | var idx = 0 76 | for await event in await self.location.startMonitoringAccuracyAuthorization() { 77 | print("Accuracy authorization did change: \(event.accuracyAuthorization.description)") 78 | XCTAssertEqual(expectedValues[idx], event.accuracyAuthorization, "Failed to get correct values from accuracy authorization status") 79 | idx += 1 80 | if idx == expectedValues.count { 81 | break 82 | } 83 | } 84 | } 85 | 86 | #if !os(tvOS) 87 | /// Test request for permission with failure in plist configuration 88 | func testRequestPermissionsFailureWithPlistConfiguration() async throws { 89 | mockLocationManager.onValidatePlistConfiguration = { permission in 90 | switch permission { 91 | case .always: 92 | return LocationErrors.plistNotConfigured 93 | case .whenInUse: 94 | return nil 95 | } 96 | } 97 | 98 | do { 99 | let newStatus = try await location.requestPermission(.always) 100 | XCTFail("Permission should fail due to missing plist while it returned \(newStatus)") 101 | } catch { } 102 | } 103 | #endif 104 | 105 | func testRequestPermissionWhenInUseSuccess() async throws { 106 | do { 107 | let expectedStatus = CLAuthorizationStatus.restricted 108 | mockLocationManager.onRequestWhenInUseAuthorization = { 109 | return expectedStatus 110 | } 111 | let newStatus = try await location.requestPermission(.whenInUse) 112 | XCTAssertEqual(expectedStatus, newStatus) 113 | } catch { 114 | XCTFail("Request should not fail: \(error.localizedDescription)") 115 | } 116 | } 117 | 118 | #if !os(tvOS) 119 | func testRequestAlwaysSuccess() async throws { 120 | do { 121 | let expectedStatus = CLAuthorizationStatus.authorizedAlways 122 | mockLocationManager.onRequestAlwaysAuthorization = { 123 | return expectedStatus 124 | } 125 | 126 | let newStatus = try await location.requestPermission(.always) 127 | XCTAssertEqual(expectedStatus, newStatus) 128 | } catch { 129 | XCTFail("Request should not fail: \(error.localizedDescription)") 130 | } 131 | } 132 | #endif 133 | 134 | /// Test the request location permission while observing authorization status change. 135 | func testMonitorAuthorizationWithPermissionRequest() async throws { 136 | mockLocationManager.authorizationStatus = .notDetermined 137 | XCTAssertEqual(location.authorizationStatus, .notDetermined) 138 | 139 | let exp = XCTestExpectation() 140 | 141 | let initialStatus = mockLocationManager.authorizationStatus 142 | Task.detached { 143 | for await event in await self.location.startMonitoringAuthorization() { 144 | print("Authorization switched from \(initialStatus) to \(event.authorizationStatus.description)") 145 | exp.fulfill() 146 | } 147 | } 148 | 149 | sleep(1) 150 | #if os(macOS) 151 | mockLocationManager.onRequestAlwaysAuthorization = { return .authorizedAlways } 152 | let newStatus = try await location.requestPermission(.always) 153 | XCTAssertEqual(newStatus, .authorizedAlways) 154 | #else 155 | mockLocationManager.onRequestWhenInUseAuthorization = { return .authorizedWhenInUse } 156 | let newStatus = try await location.requestPermission(.whenInUse) 157 | XCTAssertEqual(newStatus, .authorizedWhenInUse) 158 | #endif 159 | 160 | await fulfillment(of: [exp]) 161 | } 162 | 163 | /// Test increment of precision and monitoring. 164 | func testRequestPrecisionPosition() async throws { 165 | mockLocationManager.authorizationStatus = .notDetermined 166 | XCTAssertEqual(mockLocationManager.accuracyAuthorization, .reducedAccuracy) 167 | 168 | #if os(macOS) 169 | mockLocationManager.onRequestAlwaysAuthorization = { return .authorizedAlways } 170 | let newStatus = try await location.requestPermission(.always) 171 | XCTAssertEqual(newStatus, .authorizedAlways) 172 | #else 173 | mockLocationManager.onRequestWhenInUseAuthorization = { return .authorizedWhenInUse } 174 | let newStatus = try await location.requestPermission(.whenInUse) 175 | XCTAssertEqual(newStatus, .authorizedWhenInUse) 176 | #endif 177 | 178 | // Test misconfigured Info.plist file 179 | do { 180 | mockLocationManager.onRequestValidationForTemporaryAccuracy = { purposeKey in 181 | return LocationErrors.plistNotConfigured 182 | } 183 | let _ = try await location.requestTemporaryPrecisionAuthorization(purpose: "test") 184 | XCTFail("This should fail") 185 | } catch { 186 | XCTAssertEqual(error as? LocationErrors, LocationErrors.plistNotConfigured) 187 | } 188 | 189 | // Test correct configuration 190 | do { 191 | mockLocationManager.onRequestValidationForTemporaryAccuracy = { purposeKey in 192 | return nil 193 | } 194 | let newStatus = try await location.requestTemporaryPrecisionAuthorization(purpose: "test") 195 | XCTAssertEqual(newStatus, .fullAccuracy) 196 | } catch { 197 | XCTFail("This should not fail: \(error.localizedDescription)") 198 | } 199 | } 200 | 201 | #if !os(tvOS) 202 | /// Test stream of updates for locations. 203 | func testUpdatingLocations() async throws { 204 | // Request authorization 205 | #if os(macOS) 206 | mockLocationManager.onRequestAlwaysAuthorization = { return .authorizedAlways } 207 | try await location.requestPermission(.always) 208 | #else 209 | mockLocationManager.onRequestWhenInUseAuthorization = { return .authorizedWhenInUse } 210 | try await location.requestPermission(.whenInUse) 211 | #endif 212 | let expectedValues = simulateLocationUpdates() 213 | var idx = 0 214 | for await event in try await self.location.startMonitoringLocations() { 215 | print("Accuracy authorization did change: \(event.description)") 216 | XCTAssertEqual(expectedValues[idx], event) 217 | idx += 1 218 | if idx == expectedValues.count { 219 | break 220 | } 221 | } 222 | } 223 | #endif 224 | 225 | /// Test one shot request method. 226 | func testRequestLocation() async throws { 227 | // Request authorization 228 | #if os(macOS) 229 | mockLocationManager.onRequestAlwaysAuthorization = { return .authorizedAlways} 230 | try await location.requestPermission(.always) 231 | #else 232 | mockLocationManager.onRequestWhenInUseAuthorization = { return .authorizedWhenInUse } 233 | try await location.requestPermission(.whenInUse) 234 | #endif 235 | // Check the return of an error 236 | simulateRequestLocationDelayedResponse(event: .didFailed(LocationErrors.notAuthorized)) 237 | let e1 = try await self.location.requestLocation() 238 | XCTAssertEqual(e1.error as? LocationErrors, LocationErrors.notAuthorized) 239 | 240 | // Check the return of several location 241 | let now = Date() 242 | let l1 = CLLocation( 243 | coordinate: CLLocationCoordinate2D(latitude: 41.915001, longitude: 12.577772), 244 | altitude: 100, horizontalAccuracy: 50, verticalAccuracy: 20, timestamp: now.addingTimeInterval(-2) 245 | ) 246 | let l2 = CLLocation( 247 | coordinate: CLLocationCoordinate2D(latitude: 41.915001, longitude: 12.577772), 248 | altitude: 100, horizontalAccuracy: 50, verticalAccuracy: 20, timestamp: now.addingTimeInterval(-1) 249 | ) 250 | let l3 = CLLocation( 251 | coordinate: CLLocationCoordinate2D(latitude: 41.915001, longitude: 12.577772), 252 | altitude: 100, horizontalAccuracy: 50, verticalAccuracy: 20, timestamp: now 253 | ) 254 | simulateRequestLocationDelayedResponse(event: .didUpdateLocations([l1, l2, l3])) 255 | let e2 = try await self.location.requestLocation() 256 | XCTAssertEqual(e2.location, l3) 257 | XCTAssertEqual(e2.locations?.count, 3) 258 | 259 | 260 | // Check the timeout with a filtered location 261 | do { 262 | let nonValidLocation = CLLocation( 263 | coordinate: CLLocationCoordinate2D(latitude: 41.915001, longitude: 12.577772), 264 | altitude: 100, horizontalAccuracy: 50, verticalAccuracy: 20, timestamp: Date() 265 | ) 266 | simulateRequestLocationDelayedResponse(event: .didUpdateLocations([nonValidLocation])) 267 | 268 | let _ = try await self.location.requestLocation(accuracy: [ 269 | .horizontal(100) 270 | ], timeout: 2) 271 | } catch { 272 | XCTAssertEqual(error as? LocationErrors, LocationErrors.timeout) 273 | } 274 | 275 | // Check the return of some non filtered locations 276 | do { 277 | let now = Date() 278 | let nonValidLocationByHorizontalAccuracy = CLLocation( 279 | coordinate: CLLocationCoordinate2D(latitude: 10, longitude: 5), 280 | altitude: 100, horizontalAccuracy: 220, verticalAccuracy: 2, timestamp: now 281 | ) 282 | let nonValidLocationByVerticalAccuracy = CLLocation( 283 | coordinate: CLLocationCoordinate2D(latitude: 20, longitude: 9), 284 | altitude: 100, horizontalAccuracy: 3, verticalAccuracy: 150, timestamp: now 285 | ) 286 | let validLocation1 = CLLocation( 287 | coordinate: CLLocationCoordinate2D(latitude: 30, longitude: 12), 288 | altitude: 100, horizontalAccuracy: 1, verticalAccuracy: 1, timestamp: now 289 | ) 290 | let validLocation2 = CLLocation( 291 | coordinate: CLLocationCoordinate2D(latitude: 40, longitude: 10), 292 | altitude: 100, horizontalAccuracy: 1, verticalAccuracy: 1, timestamp: now.addingTimeInterval(-6) 293 | ) 294 | 295 | simulateRequestLocationDelayedResponse(event: .didUpdateLocations([ 296 | nonValidLocationByVerticalAccuracy, 297 | validLocation1, 298 | nonValidLocationByHorizontalAccuracy, 299 | validLocation2 300 | ])) 301 | 302 | let event = try await self.location.requestLocation(accuracy: [ 303 | .horizontal(200), 304 | .vertical(100) 305 | ]) 306 | XCTAssertEqual(event.locations?.count, 2) 307 | XCTAssertEqual(event.location, validLocation1) 308 | } catch { 309 | XCTFail("Failed to retrive location: \(error.localizedDescription)") 310 | } 311 | 312 | } 313 | 314 | #if !os(watchOS) && !os(tvOS) 315 | func testMonitorCLRegion() async throws { 316 | let (expectedValues, region) = simulateRegions() 317 | var idx = 0 318 | for await event in try await self.location.startMonitoring(region: region) { 319 | print("Monitoring region event received: \(event.description)") 320 | XCTAssertEqual(expectedValues[idx], event) 321 | idx += 1 322 | if idx == expectedValues.count { 323 | break 324 | } 325 | } 326 | } 327 | #endif 328 | 329 | #if !os(watchOS) && !os(tvOS) 330 | func testMonitoringVisits() async throws { 331 | let expectedValues = simulateVisits() 332 | var idx = 0 333 | for await event in await self.location.startMonitoringVisits() { 334 | print("Monitoring region event received: \(event.description)") 335 | XCTAssertEqual(expectedValues[idx], event) 336 | idx += 1 337 | if idx == expectedValues.count { 338 | break 339 | } 340 | } 341 | } 342 | #endif 343 | 344 | #if !os(watchOS) && !os(tvOS) 345 | func testMonitoringSignificantLocationChanges() async throws { 346 | let expectedValues = simulateSignificantLocations() 347 | var idx = 0 348 | for await event in await self.location.startMonitoringSignificantLocationChanges() { 349 | print("Visits received: \(event.description)") 350 | XCTAssertEqual(expectedValues[idx], event) 351 | idx += 1 352 | if idx == expectedValues.count { 353 | break 354 | } 355 | } 356 | } 357 | #endif 358 | 359 | #if !os(tvOS) 360 | func testAllowsBackgroundLocationUpdates() async throws { 361 | location.allowsBackgroundLocationUpdates = true 362 | XCTAssertEqual(location.allowsBackgroundLocationUpdates, location.locationManager.allowsBackgroundLocationUpdates) 363 | } 364 | #endif 365 | 366 | // MARK: - Private Functions 367 | 368 | #if !os(watchOS) && !os(tvOS) 369 | private func simulateSignificantLocations() -> [Tasks.SignificantLocationMonitoring.StreamEvent] { 370 | let sequence: [Tasks.SignificantLocationMonitoring.StreamEvent] = [ 371 | .didFailWithError(LocationErrors.timeout), 372 | .didResume, 373 | .didResume, 374 | .didUpdateLocations([ 375 | CLLocation( 376 | coordinate: CLLocationCoordinate2D(latitude: 41.915001, longitude: 12.577772), 377 | altitude: 100, horizontalAccuracy: 50, verticalAccuracy: 20, timestamp: Date() 378 | ), 379 | CLLocation( 380 | coordinate: CLLocationCoordinate2D(latitude: 41.8, longitude: 12.7), 381 | altitude: 97, horizontalAccuracy: 30, verticalAccuracy: 10, timestamp: Date() 382 | ) 383 | ]) 384 | ] 385 | DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: { 386 | for event in sequence { 387 | self.mockLocationManager.updateSignificantLocation(event: event) 388 | } 389 | }) 390 | return sequence 391 | } 392 | #endif 393 | 394 | #if !os(watchOS) && !os(tvOS) 395 | private func simulateVisits() -> [Tasks.VisitsMonitoring.StreamEvent] { 396 | let sequence: [Tasks.VisitsMonitoring.StreamEvent] = [ 397 | .didVisit(CLVisit()), 398 | .didFailWithError(LocationErrors.timeout), 399 | .didVisit(CLVisit()) 400 | ] 401 | DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: { 402 | for event in sequence { 403 | self.mockLocationManager.updateVisits(event: event) 404 | } 405 | }) 406 | return sequence 407 | } 408 | #endif 409 | 410 | #if !os(watchOS) && !os(tvOS) 411 | private func simulateRegions() -> (sequence: [Tasks.RegionMonitoring.StreamEvent], region: CLRegion) { 412 | let region = CLBeaconRegion(uuid: UUID(), identifier: "beacon_1") 413 | let sequence: [Tasks.RegionMonitoring.StreamEvent] = [ 414 | .didStartMonitoringFor(region: region), 415 | .didEnterTo(region: region), 416 | .didExitTo(region: region), 417 | .monitoringDidFailFor(region: region, error: LocationErrors.timeout) 418 | ] 419 | DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: { 420 | for event in sequence { 421 | self.mockLocationManager.updateRegionMonitoring(event: event) 422 | } 423 | }) 424 | return (sequence, region) 425 | } 426 | #endif 427 | 428 | private func simulateRequestLocationDelayedResponse(event: Tasks.ContinuousUpdateLocation.StreamEvent) { 429 | DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: { 430 | self.mockLocationManager.updateLocations(event: event) 431 | }) 432 | } 433 | 434 | private func simulateLocationUpdates() -> [Tasks.ContinuousUpdateLocation.StreamEvent] { 435 | #if os(iOS) 436 | let sequence: [Tasks.ContinuousUpdateLocation.StreamEvent] = [ 437 | .didUpdateLocations([ 438 | CLLocation( 439 | coordinate: CLLocationCoordinate2D(latitude: 41.915001, longitude: 12.577772), 440 | altitude: 100, horizontalAccuracy: 50, verticalAccuracy: 20, timestamp: Date() 441 | ), 442 | CLLocation( 443 | coordinate: CLLocationCoordinate2D(latitude: 41.8, longitude: 12.7), 444 | altitude: 97, horizontalAccuracy: 30, verticalAccuracy: 10, timestamp: Date() 445 | ) 446 | ]), 447 | .didPaused, 448 | .didResume, 449 | .didFailed(LocationErrors.notAuthorized), 450 | .didUpdateLocations([ 451 | CLLocation( 452 | coordinate: CLLocationCoordinate2D(latitude: 40, longitude: 13), 453 | altitude: 4, horizontalAccuracy: 1, verticalAccuracy: 2, timestamp: Date() 454 | ), 455 | CLLocation( 456 | coordinate: CLLocationCoordinate2D(latitude: 39, longitude: 15), 457 | altitude: 1300, horizontalAccuracy: 300, verticalAccuracy: 1, timestamp: Date() 458 | ) 459 | ]), 460 | .didUpdateLocations([ 461 | CLLocation( 462 | coordinate: CLLocationCoordinate2D(latitude: 10, longitude: 20), 463 | altitude: 10, horizontalAccuracy: 30, verticalAccuracy: 20, timestamp: Date() 464 | ) 465 | ]) 466 | ] 467 | #else 468 | let sequence: [Tasks.ContinuousUpdateLocation.StreamEvent] = [ 469 | .didUpdateLocations([ 470 | CLLocation( 471 | coordinate: CLLocationCoordinate2D(latitude: 41.915001, longitude: 12.577772), 472 | altitude: 100, horizontalAccuracy: 50, verticalAccuracy: 20, timestamp: Date() 473 | ), 474 | CLLocation( 475 | coordinate: CLLocationCoordinate2D(latitude: 41.8, longitude: 12.7), 476 | altitude: 97, horizontalAccuracy: 30, verticalAccuracy: 10, timestamp: Date() 477 | ) 478 | ]), 479 | .didFailed(LocationErrors.notAuthorized), 480 | .didUpdateLocations([ 481 | CLLocation( 482 | coordinate: CLLocationCoordinate2D(latitude: 40, longitude: 13), 483 | altitude: 4, horizontalAccuracy: 1, verticalAccuracy: 2, timestamp: Date() 484 | ), 485 | CLLocation( 486 | coordinate: CLLocationCoordinate2D(latitude: 39, longitude: 15), 487 | altitude: 1300, horizontalAccuracy: 300, verticalAccuracy: 1, timestamp: Date() 488 | ) 489 | ]), 490 | .didUpdateLocations([ 491 | CLLocation( 492 | coordinate: CLLocationCoordinate2D(latitude: 10, longitude: 20), 493 | altitude: 10, horizontalAccuracy: 30, verticalAccuracy: 20, timestamp: Date() 494 | ) 495 | ]) 496 | ] 497 | #endif 498 | 499 | DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: { 500 | for event in sequence { 501 | self.mockLocationManager.updateLocations(event: event) 502 | usleep(10) // 0.1s 503 | } 504 | }) 505 | 506 | return sequence 507 | } 508 | 509 | 510 | private func simulateAccuracyAuthorizationChanges() -> [CLAccuracyAuthorization] { 511 | let sequence : [CLAccuracyAuthorization] = [.fullAccuracy, .fullAccuracy, .fullAccuracy, .reducedAccuracy] 512 | DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: { 513 | for value in sequence { 514 | self.mockLocationManager.accuracyAuthorization = value 515 | usleep(10) // 0.1s 516 | } 517 | }) 518 | return [.fullAccuracy, .reducedAccuracy] 519 | } 520 | 521 | private func simulateAuthorizationStatusChanges() -> [CLAuthorizationStatus] { 522 | #if os(macOS) 523 | let sequence : [CLAuthorizationStatus] = [.notDetermined, .restricted, .denied, .denied, .authorizedAlways] 524 | #else 525 | let sequence : [CLAuthorizationStatus] = [.notDetermined, .restricted, .denied, .denied, .authorizedWhenInUse, .authorizedAlways] 526 | #endif 527 | DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: { 528 | for value in sequence { 529 | self.mockLocationManager.authorizationStatus = value 530 | usleep(10) // 0.1s 531 | } 532 | }) 533 | 534 | #if os(macOS) 535 | return [.restricted, .denied, .authorizedAlways] 536 | #else 537 | return [.restricted, .denied, .authorizedWhenInUse, .authorizedAlways] 538 | #endif 539 | } 540 | 541 | private func simulateLocationServicesChanges() -> [Bool] { 542 | let sequence = [false, true, false, true, true, true, false] // only real changes are detected 543 | DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: { 544 | for value in sequence { 545 | self.mockLocationManager.isLocationServicesEnabled = value 546 | usleep(10) 547 | } 548 | }) 549 | return [false, true, false, true, false] 550 | } 551 | 552 | } 553 | --------------------------------------------------------------------------------