Created
December 11, 2023 19:33
-
-
Save saroar/6de5688775745db23d33a75fe8c26680 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import ComposableArchitecture | |
import CoreLocation | |
import ComposableCoreLocation | |
import MapKit | |
import AddaSharedModels | |
extension String: LocalizedError { | |
public var errorDescription: String? { return self } | |
} | |
extension LocationManager: TestDependencyKey { | |
public static let previewValue: Self = .live | |
public static let testValue: Self = .failing | |
} | |
extension LocationManager: DependencyKey { | |
public static let liveValue: Self = .live | |
} | |
extension DependencyValues { | |
public var locationManager: LocationManager { | |
get { self[LocationManager.self] } | |
set { self[LocationManager.self] = newValue } | |
} | |
} | |
//// Extend AnyPublisher to add an async method | |
//extension AnyPublisher where Output == Never, Failure == Never { | |
// | |
// func async() async { | |
// await withCheckedContinuation { continuation in | |
// let cancellable = self.sink(receiveCompletion: { _ in | |
// continuation.resume() | |
// }, receiveValue: { _ in | |
// // This will never be called because Output == Never | |
// }) | |
// | |
// // Store the cancellable if you need to cancel the subscription later | |
// _ = cancellable | |
// } | |
// } | |
//} | |
extension Publisher { | |
func values() async throws -> AsyncThrowingStream<Output, Error> { | |
AsyncThrowingStream { continuation in | |
let cancellable = self.sink { completion in | |
switch completion { | |
case .finished: | |
continuation.finish() | |
case .failure(let error): | |
continuation.finish(throwing: error) | |
} | |
} receiveValue: { value in | |
continuation.yield(value) | |
} | |
continuation.onTermination = { @Sendable _ in | |
cancellable.cancel() | |
} | |
} | |
} | |
} | |
extension LocationManager { | |
public func locationManagerActions() -> AsyncStream<LocationManager.Action> { | |
let publisher = delegate() | |
return AsyncStream<LocationManager.Action> { continuation in | |
let cancellable = publisher.sink { action in | |
continuation.yield(action) | |
} | |
continuation.onTermination = { @Sendable _ in | |
cancellable.cancel() | |
} | |
} | |
} | |
public func requestWhenInUseAuthorizationAsync() async { | |
await withCheckedContinuation { continuation in | |
// Subscribe to the publisher and handle the completion | |
let cancellable = self.requestWhenInUseAuthorization().sink( | |
receiveCompletion: { _ in | |
// Resume the continuation when the publisher completes | |
continuation.resume() | |
}, | |
receiveValue: { _ in | |
// This will never be called because Output == Never | |
} | |
) | |
// Store the cancellable if you need to cancel the subscription later | |
_ = cancellable | |
} | |
} | |
public func requestAlwaysAuthorizationAsync() async { | |
await withCheckedContinuation { continuation in | |
// Subscribe to the publisher and handle the completion | |
let cancellable = self.requestAlwaysAuthorization().sink( | |
receiveCompletion: { _ in | |
// Resume the continuation when the publisher completes | |
continuation.resume() | |
}, | |
receiveValue: { _ in | |
// This will never be called because Output == Never | |
} | |
) | |
// Store the cancellable if you need to cancel the subscription later | |
_ = cancellable | |
} | |
} | |
} | |
extension LocationReducer.State { | |
public static let diff = Self( | |
coordinate: CLLocation(latitude: 60.020532228306031, longitude: 30.388014239849944), | |
isLocationAuthorized: true | |
) | |
} | |
public struct LocationReducer: Reducer { | |
public struct ID: Hashable, @unchecked Sendable { | |
let rawValue: AnyHashable | |
init<RawValue: Hashable & Sendable>(_ rawValue: RawValue) { | |
self.rawValue = rawValue | |
} | |
public init() { | |
struct RawValue: Hashable, Sendable {} | |
self.rawValue = RawValue() | |
} | |
} | |
public struct State: Equatable { | |
public var alert: AlertState<Action>? | |
public var coordinate: CLLocation? | |
public var isLocationAuthorized = false | |
public var waitingForUpdateLocation = false | |
public var isRequestingCurrentLocation = false | |
public var isConnected = false | |
public var location: Location? = nil | |
public var placeMark: Placemark? = nil | |
public init( | |
alert: AlertState<LocationReducer.Action>? = nil, | |
coordinate: CLLocation? = nil, | |
isLocationAuthorized: Bool = false, | |
waitingForUpdateLocation: Bool = false, | |
isRequestingCurrentLocation: Bool = false, | |
isConnected: Bool = false, | |
location: Location? = nil, | |
placeMark: Placemark? = nil | |
) { | |
self.alert = alert | |
self.coordinate = coordinate | |
self.isLocationAuthorized = isLocationAuthorized | |
self.waitingForUpdateLocation = waitingForUpdateLocation | |
self.isRequestingCurrentLocation = isRequestingCurrentLocation | |
self.isConnected = isConnected | |
self.location = location | |
self.placeMark = placeMark | |
} | |
} | |
public enum Action: Equatable { | |
case callDelegateThenGetLocation | |
case locationManager(LocationManager.Action) | |
case delegate | |
case getLocation | |
case askLocationPermission | |
case tearDown | |
case placeMarkResponse(TaskResult<Placemark>) | |
} | |
@Dependency(\.locationManager) var locationManager | |
@Dependency(\.mainQueue) var mainQueue | |
public init() {} | |
public func reduce(into state: inout State, action: Action) -> Effect<Action> { | |
@Sendable func getPlacemark(_ location: Location) async throws -> Placemark { | |
do { | |
let address = CLGeocoder() | |
let placemarks = try await address.reverseGeocodeLocation(location.rawValue) | |
let new_placemark: MKPlacemark = MKPlacemark(placemark: placemarks[0]) | |
return Placemark(rawValue: new_placemark) | |
} catch { | |
throw("Placemark is empty") | |
} | |
} | |
switch action { | |
case .callDelegateThenGetLocation: | |
return .run { send in | |
await send(.delegate) | |
await send(.getLocation) | |
} | |
case let .locationManager(.didUpdateLocations(locations)): | |
state.isRequestingCurrentLocation = false | |
guard let location = locations.first else { return .none } | |
state.location = location | |
return .run { send in | |
await send(.placeMarkResponse( | |
await TaskResult { | |
try await getPlacemark(location) | |
} | |
)) | |
} | |
case let .locationManager(.didChangeAuthorization(status)): | |
switch status { | |
case .notDetermined: | |
state.alert = .init( | |
title: TextState( | |
""" | |
Please note without location access we will not able to show any events. | |
To give us access to your location in settings. | |
""" | |
) | |
) | |
return .none | |
case .restricted, .denied: | |
state.isLocationAuthorized = false | |
state.alert = .init( | |
title: TextState( | |
""" | |
Please note without location access we will not able to show any events. | |
To give us access to your location in settings. | |
""" | |
) | |
) | |
return .none | |
case .authorizedAlways, .authorizedWhenInUse: | |
state.isLocationAuthorized = true | |
return .run { send in | |
await send(.getLocation) | |
} | |
@unknown default: | |
state.alert = .init( | |
title: TextState( | |
""" | |
Please note without location access we will not able to show any events. | |
To give us access to your location in settings. | |
""" | |
) | |
) | |
return .none | |
} | |
case .locationManager: | |
return .none | |
case .delegate: | |
return .run { send in | |
for try await action in try await locationManager.delegate().values() { | |
await send(.locationManager(action)) | |
} | |
} | |
.cancellable(id: LocationReducer.ID()) | |
case .tearDown: | |
return .cancel(id: LocationReducer.ID()) | |
case .askLocationPermission: | |
return .run { _ in | |
#if os(macOS) | |
await locationManager.requestAlwaysAuthorizationAsync() | |
#else | |
await locationManager.requestWhenInUseAuthorizationAsync() //requestWhenInUseAuthorizationAsync() | |
#endif | |
} | |
case .getLocation: | |
switch locationManager.authorizationStatus() { | |
case .notDetermined: | |
state.isRequestingCurrentLocation = true | |
state.waitingForUpdateLocation = true | |
return .run { _ in | |
#if os(macOS) | |
await locationManager.requestAlwaysAuthorization().async() | |
#else | |
await locationManager.requestWhenInUseAuthorizationAsync() | |
#endif | |
} | |
case .restricted, .denied: | |
state.isLocationAuthorized = false | |
state.alert = .init(title: TextState("Please give us access to your location in settings.")) | |
return .none | |
case .authorizedAlways, .authorizedWhenInUse: | |
state.isLocationAuthorized = true | |
state.isConnected = true | |
state.waitingForUpdateLocation = false | |
return .run { _ in | |
// await locationManager.startUpdatingLocationAsync() | |
} | |
@unknown default: | |
return .none | |
} | |
case let .placeMarkResponse(.success(placemarkNewValue)): | |
state.placeMark = placemarkNewValue | |
return .cancel(id: LocationReducer.ID()) | |
case .placeMarkResponse(.failure): | |
// handle error | |
return .none | |
} | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment