Skip to content

Instantly share code, notes, and snippets.

@robertmryan
Last active December 25, 2022 16:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save robertmryan/e70aef8595fd79ad30f773c876d155a5 to your computer and use it in GitHub Desktop.
Save robertmryan/e70aef8595fd79ad30f773c876d155a5 to your computer and use it in GitHub Desktop.
AsyncSequence/AsyncStream for CLLocationManager
//
// LocationsManager.swift
// MyApp
//
// Created by Robert Ryan on 12/24/22.
//
import Foundation
import CoreLocation
// MARK: - Public interface
// As an aside, you wouldn't usually put a manager class on the main actor, but
// `CLLocationManager` uses the current `RunLoop`, so we want to make an exception
// in this case and put it on the `@MainActor`.
@MainActor
class LocationsManager: NSObject {
private let locationManager = CLLocationManager()
private var handlers: [Handler] = []
/// The most recently retrieved user location
///
/// The value of this property is `nil` if no location data has ever been retrieved.
///
/// In iOS 4.0 and later, this property may contain a more recent location object at launch time. Specifically, if significant location updates are running and your app is terminated, this property is updated with the most recent location data when your app is relaunched (and you create a new location manager object). This location data may be more recent than the last location event processed by your app.
///
/// It is always a good idea to check the `timestamp` of the location stored in this property. If the receiver is currently gathering location data, but the minimum distance filter is large, the returned location might be relatively old. If it is, you can stop the receiver and start it again to force an update.
var location: CLLocation? { locationManager.location }
init(
desiredAccuracy: CLLocationAccuracy = kCLLocationAccuracyBest,
distanceFilter: CLLocationDistance = kCLDistanceFilterNone
) {
super.init()
locationManager.delegate = self
locationManager.desiredAccuracy = desiredAccuracy
locationManager.distanceFilter = distanceFilter
}
func locations() -> AsyncThrowingStream<CLLocation, Error> {
let stream = AsyncThrowingStream<CLLocation, Error> { continuation in
let handler = Handler(
didUpdateLocation: { continuation.yield($0) },
didFailWithError: { continuation.finish(throwing: $0) }
)
handlers.append(handler)
continuation.onTermination = { _ in
Task { @MainActor in
self.handlers.removeAll { $0.id == handler.id }
if self.handlers.isEmpty {
self.stop()
}
}
}
}
start()
return stream
}
func update(for location: CLLocation) async throws {
print("update 1")
try await Task.sleep(for: .seconds(10))
print("update 2")
}
func start() {
if locationManager.authorizationStatus == .notDetermined {
locationManager.requestWhenInUseAuthorization()
} else {
locationManager.startUpdatingLocation()
}
}
func stop() {
locationManager.stopUpdatingLocation()
}
}
// MARK: - LocationSequenceManager.Handler
extension LocationsManager {
struct Handler: Identifiable {
let id = UUID()
let didUpdateLocation: (CLLocation) -> Void
let didFailWithError: (Error) -> Void
}
}
// MARK: - CLLocationManagerDelegate
extension LocationsManager: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
locations
.forEach { location in
handlers.forEach { $0.didUpdateLocation(location) }
}
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
handlers.forEach { $0.didFailWithError(error) }
}
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
let status = manager.authorizationStatus
if status == .authorizedAlways || status == .authorizedWhenInUse {
manager.startUpdatingLocation()
}
}
}
@robertmryan
Copy link
Author

robertmryan commented Dec 22, 2022

See WWDC 2021 Meet AsyncSequence.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment