├── .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 |
5 |
6 |
7 |
8 | [](#installation)
9 | [](https://img.shields.io/badge/Swift-5.5_5.6_5.7_5.8_5.9-Orange?style=flat-square)
10 | [](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 |
--------------------------------------------------------------------------------