├── .gitignore
├── .spi.yml
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ └── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── LICENSE
├── Package.swift
├── README.md
├── Sources
└── d3-async-location
│ ├── LocationManagerAsync+
│ ├── Delegate.swift
│ └── Permission.swift
│ ├── enum
│ ├── AsyncLocationErrors.swift
│ └── LocationStreamingState.swift
│ ├── helper
│ ├── AsyncFIFOQueue.swift
│ ├── LocationManager.swift
│ └── Strategies.swift
│ ├── protocol
│ ├── ILocationDelegate.swift
│ ├── ILocationManager.swift
│ ├── ILocationResultStrategy.swift
│ └── ILocationStreamer.swift
│ └── service
│ ├── LocationStreamer.swift
│ └── ObservableLocationStreamer.swift
├── Tests
└── d3-async-locationTests
│ └── d3_async_locationTests.swift
└── img
├── apple_watch_swiftui.gif
├── image11.gif
├── image2.png
└── image6.png
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 | DerivedData/
7 | .swiftpm/config/registries.json
8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
9 | .netrc
10 |
--------------------------------------------------------------------------------
/.spi.yml:
--------------------------------------------------------------------------------
1 | version: 1
2 | builder:
3 | configs:
4 | - documentation_targets: [swift-async-corelocation-streamer]
5 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Igor Shelopaev
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.6
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: "d3-async-location",
8 | platforms: [
9 | .iOS(.v14), .watchOS(.v7),
10 | ],
11 | products: [
12 | // Products define the executables and libraries a package produces, and make them visible to other packages.
13 | .library(
14 | name: "d3-async-location",
15 | targets: ["d3-async-location"]),
16 | ],
17 | dependencies: [
18 | // Dependencies declare other packages that this package depends on.
19 | // .package(url: /* package url */, from: "1.0.0"),
20 | ],
21 | targets: [
22 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
23 | // Targets can depend on other targets in this package, and on products in packages this package depends on.
24 | .target(
25 | name: "d3-async-location",
26 | dependencies: []),
27 | .testTarget(
28 | name: "d3-async-locationTests",
29 | dependencies: ["d3-async-location"]),
30 | ]
31 | )
32 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Async location streamer using new concurrency
2 |
3 | ### Please star the repository if you believe continuing the development of this package is worthwhile. This will help me understand which package deserves more effort.
4 |
5 | [](https://swiftpackageindex.com/swiftuiux/swift-async-corelocation-streamer) [](https://swiftpackageindex.com/swiftuiux/swift-async-corelocation-streamer)
6 |
7 | This package uses Core Location under the hood to harness the power of GPS for accurate and efficient location tracking. By leveraging Swift’s async/await concurrency model, it provides a modern, clean, and scalable way to stream GPS data asynchronously. Core Location works with GPS, Wi-Fi, and cellular networks to determine a device’s position, ensuring both high accuracy and optimal performance.
8 |
9 | ## [SwiftUI example](https://github.com/swiftuiux/corelocation-manager-tracker-swift-apple-maps-example)
10 |
11 | Available for watchOS
12 |
13 | 
14 |
15 |
16 |
17 | if you are using the simulator don't forget to simulate locations
18 |
19 | 
20 |
21 | ## Features
22 | - [x] Using new concurrency swift model around CoreLocation manager
23 | - [x] Extend API to allow customization of `CLLocationManager`
24 | - [x] Support for iOS from 14.1 and watchOS from 7.0
25 | - [x] Seamless SwiftUI Integration Uses `@Published` properties for real-time UI updates or @observable is you can afford iOS17 or newer.
26 | - [x] Streaming current location asynchronously
27 | - [x] Different strategies - Keep and publish all stack of locations since streaming has started or the last one
28 | - [x] Errors handling (as **AsyncLocationErrors** so CoreLocation errors **CLError**)
29 |
30 |
31 | ## How to use
32 |
33 | ### 1. Add to info the option "Privacy - Location When In Use Usage Description"
34 | 
35 |
36 | **Background Updates**
37 | - Ensure the app has `location` included in `UIBackgroundModes` for background updates to function.
38 |
39 | ### 2. Add or inject LocationStreamer into a View
40 |
41 | ```
42 | @StateObject var service: LocationStreamer
43 | ```
44 | For iOS 17+ and watchOS 10+, using @State macro:
45 | ```
46 | @State var service: ObservableLocationStreamer
47 | ```
48 |
49 | ### 3. Call LocationStreamer method start() within async environment or check SwiftUI example
50 | ```
51 | try await service.start()
52 | ```
53 |
54 | ### LocationStreamer parameters
55 |
56 | |Param|Description|
57 | | --- | --- |
58 | |strategy| Strategy for publishing locations. Default value is **KeepLastStrategy**. Another predefined option is **KeepAllStrategy**, or you can implement and test your own custom strategy by conforming to the `LocationResultStrategy` protocol. |
59 | |accuracy| The accuracy of a geographical coordinate.|
60 | |activityType| Constants indicating the type of activity associated with location updates.|
61 | |distanceFilter| A distance in meters from an existing location to trigger updates.|
62 | |backgroundUpdates| A Boolean value that indicates whether the app receives location updates when running in the background. |
63 |
64 | or
65 |
66 | |Param|Description|
67 | | --- | --- |
68 | |strategy| Strategy for publishing locations. Default value is **KeepLastStrategy**. Another predefined option is **KeepAllStrategy**, or you can implement and test your own custom strategy by conforming to the `LocationResultStrategy` protocol. |
69 | |locationManager| A pre-configured `CLLocationManager`. |
70 |
71 |
72 | ### Default location
73 | 1. Product > Scheme > Edit Scheme
74 | 2. Click Run .app
75 | 3. Option tab
76 | 4. Already checked Core Location > select your location
77 | 5. Press OK
78 |
79 | 
80 |
81 |
82 | ## Documentation(API)
83 | - You need to have Xcode 13 installed in order to have access to Documentation Compiler (DocC)
84 | - Go to Product > Build Documentation or **⌃⇧⌘ D**
85 |
--------------------------------------------------------------------------------
/Sources/d3-async-location/LocationManagerAsync+/Delegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Delegate.swift
3 | //
4 | //
5 | // Created by Igor on 07.02.2023.
6 | //
7 |
8 | import CoreLocation
9 |
10 | extension LocationManager {
11 |
12 | /// Delegate class that implements `CLLocationManagerDelegate` methods to receive location updates
13 | /// and errors from `CLLocationManager`, and forwards them into an `AsyncFIFOQueue` for asynchronous consumption.
14 | @available(iOS 14.0, watchOS 7.0, *)
15 | final class Delegate: NSObject, ILocationDelegate {
16 |
17 | typealias DelegateOutput = LocationStreamer.Output
18 |
19 | /// The `CLLocationManager` instance used to obtain location updates.
20 | private let manager: CLLocationManager
21 |
22 | /// The FIFO queue used to emit location updates or errors as an asynchronous stream.
23 | private let fifoQueue: AsyncFIFOQueue
24 |
25 | // MARK: - Lifecycle
26 |
27 | /// Initializes the delegate with specified location settings.
28 | /// - Parameters:
29 | /// - accuracy: The desired accuracy of the location data.
30 | /// - activityType: The type of user activity associated with the location updates.
31 | /// - distanceFilter: The minimum distance (in meters) the device must move before an update event is generated.
32 | /// - backgroundUpdates: A Boolean indicating whether the app should receive location updates when suspended.
33 | public init(
34 | _ accuracy: CLLocationAccuracy?,
35 | _ activityType: CLActivityType?,
36 | _ distanceFilter: CLLocationDistance?,
37 | _ backgroundUpdates: Bool = false
38 | ) {
39 | self.manager = CLLocationManager()
40 | self.fifoQueue = AsyncFIFOQueue()
41 | super.init()
42 | manager.delegate = self
43 | updateSettings(accuracy, activityType, distanceFilter, backgroundUpdates)
44 | }
45 |
46 | /// Initializes the delegate with a given `CLLocationManager` instance.
47 | /// - Parameter locationManager: The `CLLocationManager` instance to manage location updates.
48 | public init(locationManager: CLLocationManager) {
49 | self.manager = locationManager
50 | self.fifoQueue = AsyncFIFOQueue()
51 | super.init()
52 | manager.delegate = self
53 | }
54 |
55 | deinit {
56 | finish()
57 | manager.delegate = nil
58 | #if DEBUG
59 | print("deinit delegate")
60 | #endif
61 | }
62 |
63 | // MARK: - API
64 |
65 | /// Starts location streaming.
66 | /// - Returns: An async stream of location outputs.
67 | public func start() -> AsyncStream {
68 | // Initialize the stream when needed.
69 | let stream = fifoQueue.initializeQueue()
70 |
71 | manager.startUpdatingLocation()
72 | return stream
73 | }
74 |
75 | /// Stops location updates and finishes the asynchronous FIFO queue.
76 | public func finish() {
77 | fifoQueue.finish()
78 | manager.stopUpdatingLocation()
79 | }
80 |
81 | /// Requests location permissions if not already granted.
82 | /// - Throws: An error if the permission is not granted.
83 | public func permission() async throws {
84 | let permission = Permission(with: manager.authorizationStatus)
85 | try await permission.grant(for: manager)
86 | }
87 |
88 | // MARK: - Delegate Methods
89 |
90 | public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
91 | fifoQueue.enqueue(.success(locations))
92 | }
93 |
94 | public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
95 | let cleError = error as? CLError ?? CLError(.locationUnknown)
96 | fifoQueue.enqueue(.failure(cleError))
97 | }
98 |
99 | public func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
100 | NotificationCenter.default.post(name: Permission.authorizationStatus, object: manager.authorizationStatus)
101 | }
102 |
103 | // MARK: - Private Methods
104 |
105 | private func updateSettings(
106 | _ accuracy: CLLocationAccuracy?,
107 | _ activityType: CLActivityType?,
108 | _ distanceFilter: CLLocationDistance?,
109 | _ backgroundUpdates: Bool = false
110 | ) {
111 | manager.desiredAccuracy = accuracy ?? kCLLocationAccuracyBest
112 | manager.activityType = activityType ?? .other
113 | manager.distanceFilter = distanceFilter ?? kCLDistanceFilterNone
114 | #if os(iOS) || os(watchOS)
115 | manager.allowsBackgroundLocationUpdates = backgroundUpdates
116 | #endif
117 | }
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/Sources/d3-async-location/LocationManagerAsync+/Permission.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Permission.swift
3 | //
4 | //
5 | // Created by Igor on 06.02.2023.
6 | //
7 |
8 | import CoreLocation
9 | import Combine
10 | import SwiftUI
11 |
12 | extension LocationManager{
13 |
14 | /// Helper class to determine permission to get access for streaming ``CLLocation``
15 | @available(iOS 14.0, watchOS 7.0, *)
16 | final class Permission{
17 |
18 | /// Name of notification for event location manager changed authorization status
19 | static public let authorizationStatus = Notification.Name("authorizationStatus")
20 |
21 | // MARK: - Private properties
22 |
23 | /// The current authorization status for the app
24 | private var status : CLAuthorizationStatus
25 |
26 | /// Continuation to get permission if status is not defined
27 | private var flow : CheckedContinuation?
28 |
29 | /// Check if status is determined
30 | private var isDetermined : Bool{
31 | status != .notDetermined
32 | }
33 |
34 | /// Subscription to authorization status changes
35 | private var cancelable : AnyCancellable?
36 |
37 | // MARK: - Life circle
38 |
39 | /// Init defining is access to location service is granted
40 | /// - Parameter status: Constant indicating the app's authorization to use location services
41 | init(with status: CLAuthorizationStatus){
42 | self.status = status
43 | initSubscription()
44 | }
45 |
46 | /// resume continuation if it was not called with status .notDetermined
47 | deinit {
48 | resume(with: .notDetermined)
49 | }
50 |
51 | // MARK: - API
52 |
53 | /// Get status asynchronously and check is it authorized to start getting the stream of locations
54 | public func grant(for manager: CLLocationManager) async throws {
55 | let status = await requestPermission(manager)
56 | if status.isNotAuthorized{
57 | throw AsyncLocationErrors.accessIsNotAuthorized
58 | }
59 | }
60 |
61 | // MARK: - Private methods
62 |
63 | /// Subscribe for event when location manager change authorization status to go on access permission flow
64 | private func initSubscription(){
65 | let name = Permission.authorizationStatus
66 | cancelable = NotificationCenter.default.publisher(for: name)
67 | .sink { [weak self] value in
68 | self?.statusChanged(value)
69 | }
70 | }
71 |
72 | /// Determine status after the request permission
73 | /// - Parameter manager: Location manager
74 | private func statusChanged(_ value: Output) {
75 | if let s = value.object as? CLAuthorizationStatus{
76 | status = s
77 | resume(with: status)
78 | }
79 | }
80 |
81 | /// Resume continuation
82 | /// - Parameter status: resume
83 | private func resume(with status : CLAuthorizationStatus) {
84 | flow?.resume(returning: status)
85 | flow = nil
86 | }
87 |
88 | /// Requests the user’s permission to use location services while the app is in use
89 | /// Don't forget to add in Info "Privacy - Location When In Use Usage Description" something like "Show list of locations"
90 | /// - Returns: Permission status
91 | private func requestPermission(_ manager : CLLocationManager) async -> CLAuthorizationStatus{
92 | manager.requestWhenInUseAuthorization()
93 |
94 | if isDetermined{
95 | return status
96 | }
97 |
98 | return await withCheckedContinuation{ continuation in
99 | flow = continuation
100 | }
101 | }
102 | }
103 | }
104 |
105 | // MARK: - Alias types -
106 |
107 | fileprivate typealias Output = NotificationCenter.Publisher.Output
108 |
109 | // MARK: - Extensions -
110 |
111 | fileprivate extension CLAuthorizationStatus {
112 | /// Check if access is not authorized
113 | /// denied - The user denied the use of location services for the app or they are disabled globally in Settings
114 | /// restricted - The app is not authorized to use location services
115 | /// - Returns: Return `True` if it was denied
116 | var isNotAuthorized: Bool {
117 | [.denied, .restricted].contains(self)
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/Sources/d3-async-location/enum/AsyncLocationErrors.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LocationManagerErrors.swift
3 | //
4 | //
5 | // Created by Igor on 03.02.2023.
6 | //
7 |
8 | import CoreLocation
9 |
10 | /// Async locations manager errors
11 | @available(iOS 14.0, watchOS 7.0, *)
12 | public enum AsyncLocationErrors: Error{
13 | ///Access was denied by user
14 | case accessIsNotAuthorized
15 | }
16 |
17 |
18 | @available(iOS 14.0, watchOS 7.0, *)
19 | extension AsyncLocationErrors: LocalizedError {
20 |
21 | public var errorDescription: String? {
22 | switch self {
23 | case .accessIsNotAuthorized:
24 | return NSLocalizedString("Access was denied by the user.", comment: "")
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Sources/d3-async-location/enum/LocationStreamingState.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LocationStreamingState.swift
3 | //
4 | //
5 | // Created by Igor on 04.02.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | /// Streaming states
11 | @available(iOS 14.0, watchOS 7.0, *)
12 | public enum LocationStreamingState{
13 |
14 | /// not streaming
15 | case idle
16 |
17 | /// streaming has been started
18 | case streaming
19 | }
20 |
--------------------------------------------------------------------------------
/Sources/d3-async-location/helper/AsyncFIFOQueue.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AsyncFIFOQueue.swift
3 | // d3-async-location
4 | //
5 | // Created by Igor Shelopaev on 04.12.24.
6 | //
7 |
8 | import Foundation
9 |
10 | extension LocationManager {
11 |
12 | /// A generic FIFO queue that provides an asynchronous stream of elements.
13 | /// The stream is initialized lazily and can be terminated and cleaned up.
14 | @available(iOS 14.0, watchOS 7.0, *)
15 | final class AsyncFIFOQueue{
16 |
17 | /// Type alias for the AsyncStream Continuation.
18 | typealias Continuation = AsyncStream.Continuation
19 |
20 |
21 | /// The asynchronous stream that consumers can iterate over.
22 | private var stream: AsyncStream?
23 |
24 | /// The continuation used to produce values for the stream.
25 | private var continuation: Continuation?
26 |
27 | /// Initializes the stream and continuation.
28 | /// Should be called before starting to enqueue elements.
29 | /// - Parameter onTermination: An escaping closure to handle termination events.
30 | /// - Returns: The initialized `AsyncStream`.
31 | func initializeQueue() -> AsyncStream {
32 | // Return the existing stream if it's already initialized.
33 | if let existingStream = stream {
34 | return existingStream
35 | }
36 |
37 | let (newStream, newContinuation) = AsyncStream.makeStream(of: Element.self)
38 |
39 | newContinuation.onTermination = { [weak self] termination in
40 | self?.finish()
41 | }
42 |
43 | self.stream = newStream
44 | self.continuation = newContinuation
45 |
46 | return newStream
47 | }
48 |
49 | /// Provides access to the asynchronous stream.
50 | /// - Returns: The initialized `AsyncStream` instance, or `nil` if not initialized.
51 | func getQueue() -> AsyncStream? {
52 | return stream
53 | }
54 |
55 | /// Enqueues a new element into the stream.
56 | /// - Parameter element: The element to enqueue.
57 | func enqueue(_ element: Element) {
58 | continuation?.yield(element)
59 | }
60 |
61 | /// Finishes the stream, indicating no more elements will be enqueued.
62 | /// Cleans up the stream and continuation.
63 | func finish() {
64 | continuation?.finish()
65 | continuation = nil
66 | stream = nil
67 | }
68 |
69 | deinit {
70 | #if DEBUG
71 | print("deinit async queue")
72 | #endif
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/Sources/d3-async-location/helper/LocationManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LocationManagerAsync.swift
3 | //
4 | // Created by Igor on 03.02.2023.
5 | //
6 |
7 | import CoreLocation
8 |
9 | /// A location manager that streams data asynchronously using an `AsyncStream`.
10 | /// It requests permission in advance if it hasn't been determined yet.
11 | /// Use the `start()` method to begin streaming location updates.
12 | @available(iOS 14.0, watchOS 7.0, *)
13 | final class LocationManager: ILocationManager {
14 |
15 | /// The delegate responsible for handling location updates and forwarding them to the async stream.
16 | private let delegate: ILocationDelegate
17 |
18 | // MARK: - Lifecycle
19 |
20 | /// Initializes a new `LocationManagerAsync` instance with the specified settings.
21 | /// - Parameters:
22 | /// - accuracy: The desired accuracy of the location data.
23 | /// - activityType: The type of user activity associated with the location updates.
24 | /// - distanceFilter: The minimum distance (in meters) that the device must move before an update event is generated. kCLDistanceFilterNone (equivalent to -1.0) means updates are sent regardless of the distance traveled. This is a safe default for apps that don’t require filtering updates based on distance.
25 | /// - backgroundUpdates: A Boolean value indicating whether the app should receive location updates when suspended.
26 | init(
27 | _ accuracy: CLLocationAccuracy?,
28 | _ activityType: CLActivityType?,
29 | _ distanceFilter: CLLocationDistance?,
30 | _ backgroundUpdates: Bool
31 | ) {
32 | delegate = LocationManager.Delegate(accuracy, activityType, distanceFilter, backgroundUpdates)
33 | }
34 |
35 | /// Initializes the `LocationManagerAsync` instance with a specified `CLLocationManager` instance.
36 | /// - Parameter locationManager: A pre-configured `CLLocationManager` instance used to manage location updates.
37 | public init(locationManager: CLLocationManager) {
38 | delegate = LocationManager.Delegate(locationManager: locationManager)
39 | }
40 |
41 | /// Deinitializes the `LocationManagerAsync` instance, performing any necessary cleanup.
42 | deinit {
43 | #if DEBUG
44 | print("deinit manager")
45 | #endif
46 | }
47 |
48 | // MARK: - API
49 |
50 | /// Checks permission status and starts streaming location data asynchronously.
51 | /// - Returns: An `AsyncStream` emitting location updates or errors.
52 | /// - Throws: An error if permission is not granted.
53 | public func start() async throws -> AsyncStream {
54 |
55 | try await delegate.permission()
56 |
57 | return delegate.start()
58 | }
59 |
60 | /// Stops the location streaming process.
61 | public func stop() {
62 | delegate.finish()
63 |
64 | #if DEBUG
65 | print("stop updating")
66 | #endif
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/Sources/d3-async-location/helper/Strategies.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Strategies.swift
3 | //
4 | //
5 | // Created by Igor Shelopaev on 10.02.2023.
6 | //
7 |
8 | import Foundation
9 |
10 |
11 |
12 | /// A concrete strategy that keeps only the last location result.
13 | /// This strategy ensures that only the most recent location update is retained.
14 | @available(iOS 14.0, watchOS 7.0, *)
15 | public struct KeepLastStrategy: ILocationResultStrategy {
16 |
17 | /// Initializes a new instance of `KeepLastStrategy`.
18 | public init() {}
19 |
20 | /// Processes the results array by replacing it with the new result.
21 | /// - Parameters:
22 | /// - results: The current array of processed results (ignored in this strategy).
23 | /// - newResult: The new result to be stored.
24 | /// - Returns: An array containing only the new result.
25 | public func process(results: [Output], newResult: Output) -> [Output] {
26 | return [newResult]
27 | }
28 | }
29 |
30 | /// A concrete strategy that keeps all location results.
31 | /// This strategy appends each new location result to the existing array.
32 | @available(iOS 14.0, watchOS 7.0, *)
33 | public struct KeepAllStrategy: ILocationResultStrategy {
34 |
35 | /// Initializes a new instance of `KeepAllStrategy`.
36 | public init() {}
37 |
38 | /// Processes the results array by appending the new result to it.
39 | /// - Parameters:
40 | /// - results: The current array of processed results.
41 | /// - newResult: The new result to be added to the array.
42 | /// - Returns: A new array of results, including the new result appended to the existing results.
43 | public func process(results: [Output], newResult: Output) -> [Output] {
44 | var updatedResults = results
45 | updatedResults.append(newResult)
46 | return updatedResults
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/Sources/d3-async-location/protocol/ILocationDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ILocationDelegate.swift
3 | //
4 | //
5 | // Created by Igor on 05.07.2023.
6 | //
7 |
8 | import Foundation
9 | import CoreLocation
10 |
11 | /// A protocol that defines the interface for location delegates.
12 | @available(iOS 14.0, watchOS 7.0, *)
13 | public protocol ILocationDelegate: NSObjectProtocol, CLLocationManagerDelegate {
14 |
15 | /// Starts the location streaming process.
16 | ///
17 | /// - Returns: An `AsyncStream` emitting `LocationStreamer.Output` values.
18 | /// The stream provides asynchronous location updates or errors to the consumer.
19 | func start() -> AsyncStream
20 |
21 | /// Stops the location streaming process.
22 | ///
23 | /// Cleans up resources, stops receiving location updates,
24 | /// and ensures that any ongoing streams are properly terminated.
25 | func finish()
26 |
27 | /// Requests the necessary location permissions from the user if not already granted.
28 | ///
29 | /// - Throws: An error if the permission request fails or the user denies access.
30 | ///
31 | /// This method should be called before starting the location updates to ensure that the app has the required permissions.
32 | func permission() async throws
33 | }
34 |
--------------------------------------------------------------------------------
/Sources/d3-async-location/protocol/ILocationManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Igor on 03.02.2023.
6 | //
7 |
8 | import Foundation
9 | import CoreLocation
10 |
11 | @available(iOS 14.0, watchOS 7.0, *)
12 | protocol ILocationManager {
13 |
14 | /// Starts the async stream of location updates.
15 | /// - Returns: An `AsyncStream` of `Output` that emits location updates or errors.
16 | /// - Throws: An error if the streaming cannot be started.
17 | func start() async throws -> AsyncStream
18 |
19 | /// Stops the location streaming process.
20 | func stop()
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/d3-async-location/protocol/ILocationResultStrategy.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ILocationResultStrategy.swift
3 | // d3-async-location
4 | //
5 | // Created by Igor on 11.12.24.
6 | //
7 |
8 | /// A protocol that defines a strategy for processing location results.
9 | /// Implementations of this protocol determine how new location results are handled
10 | /// (e.g., whether they replace the previous results or are appended).
11 | @available(iOS 14.0, watchOS 7.0, *)
12 | public protocol ILocationResultStrategy {
13 |
14 | /// The type of output that the strategy processes.
15 | /// `Output` is defined as `LocationStreamer.Output`, which is a `Result` containing
16 | /// an array of `CLLocation` objects or a `CLError`.
17 | typealias Output = LocationStreamer.Output
18 |
19 | /// Processes the results array by incorporating a new location result.
20 | /// - Parameters:
21 | /// - results: The current array of processed results.
22 | /// - newResult: The new result to be processed and added.
23 | /// - Returns: A new array of results after applying the strategy.
24 | func process(results: [Output], newResult: Output) -> [Output]
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/d3-async-location/protocol/ILocationStreamer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ILocationManagerViewModel.swift
3 | //
4 | //
5 | // Created by Igor on 03.02.2023.
6 | //
7 |
8 | import CoreLocation
9 | import SwiftUI
10 |
11 | @available(iOS 14.0, watchOS 7.0, *)
12 | @MainActor
13 | public protocol ILocationStreamer{
14 |
15 | /// List of locations
16 | var results : [LocationStreamer.Output] { get }
17 |
18 | /// Strategy for publishing locations Default value is .keepLast
19 | var strategy : ILocationResultStrategy { get }
20 |
21 | /// Start streaming locations
22 | func start(clean: Bool) async throws
23 |
24 | /// Stop streaming locations
25 | func stop()
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/d3-async-location/service/LocationStreamer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LocationStreamer.swift
3 | //
4 | //
5 | // Created by Igor Shelopaev on 03.02.2023.
6 | //
7 |
8 | import SwiftUI
9 | import CoreLocation
10 |
11 | /// A ViewModel for asynchronously streaming location updates.
12 | /// This class leverages `ObservableObject` to publish updates for SwiftUI Views.
13 | ///
14 | /// Example usage in a View:
15 | /// ```
16 | /// @EnvironmentObject var model: LocationStreamer
17 | /// ```
18 | ///
19 | /// To start streaming updates, call the `start()` method within an async environment.
20 | ///
21 | /// - Available: iOS 14.0+, watchOS 7.0+
22 | @available(iOS 14.0, watchOS 7.0, *)
23 | public final class LocationStreamer: ILocationStreamer, ObservableObject {
24 |
25 | /// Represents the output of the location manager.
26 | /// Each output is either:
27 | /// - A list of results (`[CLLocation]` objects), or
28 | /// - A `CLError` in case of failure.
29 | public typealias Output = Result<[CLLocation], CLError>
30 |
31 | // MARK: - Public Properties
32 |
33 | /// Defines the strategy for processing and publishing location updates.
34 | /// Default strategy retains only the most recent update (`KeepLastStrategy`).
35 | public let strategy: ILocationResultStrategy
36 |
37 | /// A list of location results, published for subscribing Views.
38 | /// This property is updated based on the chosen `strategy`.
39 | @MainActor @Published public private(set) var results: [Output] = []
40 |
41 | /// Indicates the current streaming state of the ViewModel.
42 | /// State transitions include `.idle`, `.streaming`, and `.error`.
43 | @MainActor @Published public private(set) var state: LocationStreamingState = .idle
44 |
45 | // MARK: - Private Properties
46 |
47 | /// Handles the actual location updates asynchronously.
48 | private let manager: LocationManager
49 |
50 | /// Checks if the streaming process is idle.
51 | /// A computed property for convenience.
52 | @MainActor
53 | public var isIdle: Bool {
54 | return state == .idle
55 | }
56 |
57 | // MARK: - Lifecycle
58 |
59 | /// Initializes the `LocationStreamer` with configurable parameters.
60 | /// - Parameters:
61 | /// - strategy: A `LocationResultStrategy` for managing location results. Defaults to `KeepLastStrategy`.
62 | /// - accuracy: Specifies the desired accuracy of location updates. Defaults to `kCLLocationAccuracyBest`.
63 | /// - activityType: The type of activity for location updates (e.g., automotive, fitness). Defaults to `.other`.
64 | /// - distanceFilter: The minimum distance (in meters) before generating an update. Defaults to `kCLDistanceFilterNone` (no filtering).
65 | /// - backgroundUpdates: Whether the app should continue receiving location updates in the background. Defaults to `false`.
66 | public init(
67 | strategy: ILocationResultStrategy = KeepLastStrategy(),
68 | _ accuracy: CLLocationAccuracy? = kCLLocationAccuracyBest,
69 | _ activityType: CLActivityType? = .other,
70 | _ distanceFilter: CLLocationDistance? = kCLDistanceFilterNone,
71 | _ backgroundUpdates: Bool = false
72 | ) {
73 | self.strategy = strategy
74 | manager = .init(accuracy, activityType, distanceFilter, backgroundUpdates)
75 | }
76 |
77 | /// Initializes the `LocationStreamer` with a pre-configured `CLLocationManager`.
78 | /// - Parameters:
79 | /// - strategy: A `LocationResultStrategy` for managing location results. Defaults to `KeepLastStrategy`.
80 | /// - locationManager: A pre-configured `CLLocationManager` instance.
81 | public init(
82 | strategy: ILocationResultStrategy = KeepLastStrategy(),
83 | locationManager: CLLocationManager
84 | ) {
85 | self.strategy = strategy
86 | manager = .init(locationManager: locationManager)
87 | }
88 |
89 | /// Cleans up resources when the instance is deallocated.
90 | deinit {
91 | #if DEBUG
92 | print("deinit LocationStreamer")
93 | #endif
94 | }
95 |
96 | // MARK: - API
97 |
98 | /// Starts streaming location updates asynchronously.
99 | /// - Parameters:
100 | /// - clean: Whether to clear previous results before starting. Defaults to `true`.
101 | /// - Throws: `AsyncLocationErrors.streamingProcessHasAlreadyStarted` if streaming is already active.
102 | @MainActor
103 | public func start(clean: Bool = true) async throws {
104 | if state == .streaming { stop() }
105 | if clean { self.clean() }
106 |
107 | setState(.streaming)
108 |
109 | let stream = try await manager.start()
110 |
111 | for await result in stream {
112 | add(result)
113 | }
114 |
115 | stop()
116 | setState(.idle)
117 | }
118 |
119 | /// Stops the location streaming process and sets the state to idle.
120 | @MainActor
121 | public func stop() {
122 | manager.stop()
123 | setState(.idle)
124 |
125 | #if DEBUG
126 | print("stop manager")
127 | #endif
128 | }
129 |
130 | // MARK: - Private Methods
131 |
132 | /// Clears all stored results.
133 | @MainActor
134 | private func clean() {
135 | results = []
136 | }
137 |
138 | /// Adds a new location result to the `results` array.
139 | /// The behavior depends on the configured `strategy`.
140 | /// - Parameter result: The new result to be processed and added.
141 | @MainActor
142 | private func add(_ result: Output) {
143 | results = strategy.process(results: results, newResult: result)
144 | }
145 |
146 | /// Updates the streaming state of the ViewModel.
147 | /// - Parameter value: The new state to set (e.g., `.idle`, `.streaming`).
148 | @MainActor
149 | private func setState(_ value: LocationStreamingState) {
150 | state = value
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/Sources/d3-async-location/service/ObservableLocationStreamer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ObservableLocationStreamer.swift
3 | //
4 | //
5 | // Created by Igor on 03.02.2023.
6 | //
7 |
8 | import SwiftUI
9 | import CoreLocation
10 |
11 | #if compiler(>=5.9) && canImport(Observation)
12 |
13 | /// ViewModel for asynchronously posting location updates.
14 | @available(iOS 17.0, watchOS 10.0, *)
15 | @Observable
16 | public final class ObservableLocationStreamer: ILocationStreamer{
17 |
18 | /// Represents the output of the location manager.
19 | /// Contains either a list of results (e.g., `CLLocation` objects) or a `CLError` in case of failure.
20 | public typealias Output = Result<[CLLocation], CLError>
21 |
22 | // MARK: - Public Properties
23 |
24 | /// Strategy for publishing updates. Default value is `.keepLast`.
25 | public let strategy: ILocationResultStrategy
26 |
27 | /// A list of results published for subscribed Views.
28 | /// Results may include various types of data (e.g., `CLLocation` objects) depending on the implementation.
29 | /// Use this publisher to feed Views with updates or create a proxy to manipulate the flow,
30 | /// such as filtering, mapping, or dropping results.
31 | @MainActor public private(set) var results: [Output] = []
32 |
33 | /// Current streaming state of the ViewModel.
34 | @MainActor public private(set) var state: LocationStreamingState = .idle
35 |
36 | // MARK: - Private Properties
37 |
38 | /// The asynchronous locations manager responsible for streaming updates.
39 | private let manager: LocationManager
40 |
41 | /// Indicates whether the streaming process is idle.
42 | @MainActor
43 | public var isIdle: Bool {
44 | return state == .idle
45 | }
46 |
47 | // MARK: - Lifecycle
48 |
49 | /// Initializes the `LocationStreamer` with configurable parameters.
50 | /// - Parameters:
51 | /// - strategy: A `LocationResultStrategy` for managing location results. Defaults to `KeepLastStrategy`.
52 | /// - accuracy: Specifies the desired accuracy of location updates. Defaults to `kCLLocationAccuracyBest`.
53 | /// - activityType: The type of activity for location updates (e.g., automotive, fitness). Defaults to `.other`.
54 | /// - distanceFilter: The minimum distance (in meters) before generating an update. Defaults to `kCLDistanceFilterNone` (no filtering).
55 | /// - backgroundUpdates: Whether the app should continue receiving location updates in the background. Defaults to `false`.
56 | public init(
57 | strategy: ILocationResultStrategy = KeepLastStrategy(),
58 | _ accuracy: CLLocationAccuracy? = kCLLocationAccuracyBest,
59 | _ activityType: CLActivityType? = .other,
60 | _ distanceFilter: CLLocationDistance? = kCLDistanceFilterNone,
61 | _ backgroundUpdates: Bool = false
62 | ) {
63 | self.strategy = strategy
64 | manager = .init(accuracy, activityType, distanceFilter, backgroundUpdates)
65 | }
66 |
67 | /// Initializes the `LocationStreamer` with a pre-configured `CLLocationManager`.
68 | /// - Parameters:
69 | /// - strategy: A `LocationResultStrategy` for managing location results. Defaults to `KeepLastStrategy`.
70 | /// - locationManager: A pre-configured `CLLocationManager` instance.
71 | public init(
72 | strategy: ILocationResultStrategy = KeepLastStrategy(),
73 | locationManager: CLLocationManager
74 | ) {
75 | self.strategy = strategy
76 | manager = .init(locationManager: locationManager)
77 | }
78 |
79 | /// Cleans up resources when the instance is deallocated.
80 | deinit {
81 | #if DEBUG
82 | print("deinit LocationStreamer")
83 | #endif
84 | }
85 |
86 | // MARK: - API
87 |
88 | /// Starts streaming location updates asynchronously.
89 | /// - Parameters:
90 | /// - clean: Whether to clear previous results before starting. Defaults to `true`.
91 | /// - Throws: `AsyncLocationErrors.streamingProcessHasAlreadyStarted` if streaming is already active.
92 | @MainActor public func start(clean: Bool = true) async throws {
93 | if state == .streaming { stop() }
94 | if clean { self.clean() }
95 |
96 | setState(.streaming)
97 |
98 | let stream = try await manager.start()
99 |
100 | for await result in stream {
101 | add(result)
102 | }
103 | setState(.idle)
104 | }
105 |
106 | /// Stops the location streaming process and sets the state to idle.
107 | @MainActor public func stop() {
108 | manager.stop()
109 | setState(.idle)
110 |
111 | #if DEBUG
112 | print("stop manager")
113 | #endif
114 | }
115 |
116 | // MARK: - Private Methods
117 |
118 | /// Clears all stored results.
119 | @MainActor
120 | private func clean() {
121 | results = []
122 | }
123 |
124 | /// Adds a new location result to the `results` array.
125 | /// The behavior depends on the configured `strategy`.
126 | /// - Parameter result: The new result to be processed and added.
127 | @MainActor
128 | private func add(_ result: Output) {
129 | results = strategy.process(results: results, newResult: result)
130 | }
131 |
132 | /// Updates the streaming state of the ViewModel.
133 | /// - Parameter value: The new state to set (e.g., `.idle`, `.streaming`).
134 | @MainActor
135 | private func setState(_ value: LocationStreamingState) {
136 | state = value
137 | }
138 | }
139 |
140 | #endif
141 |
--------------------------------------------------------------------------------
/Tests/d3-async-locationTests/d3_async_locationTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import d3_async_location
3 |
4 | final class d3_async_locationTests: XCTestCase {
5 | func testExample() throws {
6 | // This is an example of a functional test case.
7 | // Use XCTAssert and related functions to verify your tests produce the correct
8 | // results.
9 | // XCTAssertEqual(d3_async_location().text, "Hello, World!")
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/img/apple_watch_swiftui.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swiftuiux/swift-async-corelocation-streamer/HEAD/img/apple_watch_swiftui.gif
--------------------------------------------------------------------------------
/img/image11.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swiftuiux/swift-async-corelocation-streamer/HEAD/img/image11.gif
--------------------------------------------------------------------------------
/img/image2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swiftuiux/swift-async-corelocation-streamer/HEAD/img/image2.png
--------------------------------------------------------------------------------
/img/image6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swiftuiux/swift-async-corelocation-streamer/HEAD/img/image6.png
--------------------------------------------------------------------------------