Last active
December 18, 2024 19:56
-
-
Save emrdgrmnci/2f01bf5afca63c981a731f503d808e8b 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 SwiftUI | |
import ComposableArchitecture | |
import CoreLocation | |
struct LocationPermissionFeature: Reducer { | |
struct State: Equatable { | |
var authorizationStatus: CLAuthorizationStatus = .notDetermined | |
var currentLocation: CLLocation? | |
var showLocationAlert = false | |
var locationAlertTitle = "" | |
var locationAlertMessage = "" | |
} | |
enum Action: Equatable { | |
case onAppear | |
case locationManager(LocationManagerClient.Action) | |
case requestLocationPermission | |
case startTrackingLocation | |
case showLocationPermissionAlert(title: String, message: String) | |
case dismissLocationAlert | |
case openSettings | |
} | |
@Dependency(\.locationManager) var locationManager | |
var body: some Reducer<State, Action> { | |
Reduce { state, action in | |
switch action { | |
case .onAppear: | |
return .run { send in | |
// Start monitoring authorization changes | |
let status = await locationManager.authorizationStatus() | |
await send(.locationManager(.didChangeAuthorization(status))) | |
// If already authorized, start tracking | |
if status == .authorizedWhenInUse || status == .authorizedAlways { | |
await send(.startTrackingLocation) | |
} | |
// Listen for all location events | |
for await event in locationManager.delegate() { | |
await send(.locationManager(event)) | |
} | |
} | |
case .startTrackingLocation: | |
return .run { send in | |
// Start getting location updates | |
let locationStream = await locationManager.startUpdatingLocation() | |
for await locations in locationStream { | |
await send(.locationManager(.didUpdateLocations(locations))) | |
} | |
} | |
case let .locationManager(.didUpdateLocations(locations)): | |
if let location = locations.last { | |
state.currentLocation = location | |
print("📍 Location updated: \(location.coordinate)") | |
} | |
return .none | |
case let .locationManager(.didChangeAuthorization(status)): | |
state.authorizationStatus = status | |
// Automatically start tracking when authorized | |
if status == .authorizedWhenInUse || status == .authorizedAlways { | |
return .send(.startTrackingLocation) | |
} | |
return .none | |
case .requestLocationPermission: | |
return .run { send in | |
let servicesEnabled = await locationManager.locationServicesEnabled() | |
if !servicesEnabled { | |
await send(.showLocationPermissionAlert( | |
title: "Location Services Disabled", | |
message: "Please enable Location Services in Settings to use this feature." | |
)) | |
return | |
} | |
let status = await locationManager.authorizationStatus() | |
switch status { | |
case .notDetermined: | |
await locationManager.requestWhenInUseAuthorization() | |
case .denied: | |
await send(.showLocationPermissionAlert( | |
title: "Location Access Required", | |
message: "Please enable location access in Settings to use all features of the app." | |
)) | |
case .restricted: | |
await send(.showLocationPermissionAlert( | |
title: "Location Access Restricted", | |
message: "Location access is restricted. This may be due to parental controls." | |
)) | |
case .authorizedWhenInUse, .authorizedAlways: | |
await locationManager.requestWhenInUseAuthorization() | |
await send(.startTrackingLocation) | |
@unknown default: | |
break | |
} | |
} | |
case let .showLocationPermissionAlert(title, message): | |
state.showLocationAlert = true | |
state.locationAlertTitle = title | |
state.locationAlertMessage = message | |
return .none | |
case .dismissLocationAlert: | |
state.showLocationAlert = false | |
return .none | |
case .openSettings: | |
return .run { _ in | |
guard let settingsUrl = URL(string: UIApplication.openSettingsURLString) else { | |
return | |
} | |
await UIApplication.shared.open(settingsUrl) | |
} | |
} | |
} | |
} | |
} | |
// MARK: - Location Manager Client | |
struct LocationManagerClient { | |
var delegate: @Sendable () -> AsyncStream<Action> | |
var requestWhenInUseAuthorization: @Sendable @MainActor () async -> Void | |
var startUpdatingLocation: @Sendable @MainActor () -> AsyncStream<[CLLocation]> | |
var authorizationStatus: @Sendable @MainActor () -> CLAuthorizationStatus | |
var locationServicesEnabled: @Sendable @MainActor () -> Bool | |
enum Action: Equatable { | |
case didChangeAuthorization(CLAuthorizationStatus) | |
case didUpdateLocations([CLLocation]) | |
static func == (lhs: Self, rhs: Self) -> Bool { | |
switch (lhs, rhs) { | |
case let (.didChangeAuthorization(lhs), .didChangeAuthorization(rhs)): | |
return lhs == rhs | |
case let (.didUpdateLocations(lhs), .didUpdateLocations(rhs)): | |
return zip(lhs, rhs).allSatisfy { lhsLocation, rhsLocation in | |
lhsLocation.coordinate.latitude == rhsLocation.coordinate.latitude && | |
lhsLocation.coordinate.longitude == rhsLocation.coordinate.longitude | |
} | |
case (.didUpdateLocations, .didChangeAuthorization), | |
(.didChangeAuthorization, .didUpdateLocations): | |
return false | |
} | |
} | |
} | |
} | |
// MARK: - Live Implementation | |
extension LocationManagerClient: DependencyKey { | |
static var liveValue: Self { | |
let manager = CLLocationManager() | |
manager.desiredAccuracy = kCLLocationAccuracyBest | |
manager.distanceFilter = 10 | |
return Self( | |
delegate: { | |
AsyncStream { continuation in | |
let delegate = LocationDelegate(continuation: continuation) | |
manager.delegate = delegate | |
continuation.onTermination = { _ in | |
manager.delegate = nil | |
} | |
} | |
}, | |
requestWhenInUseAuthorization: { | |
manager.requestWhenInUseAuthorization() | |
}, | |
startUpdatingLocation: { | |
AsyncStream { continuation in | |
manager.startUpdatingLocation() | |
continuation.onTermination = { _ in | |
manager.stopUpdatingLocation() | |
} | |
} | |
}, | |
authorizationStatus: { | |
manager.authorizationStatus | |
}, | |
locationServicesEnabled: { | |
CLLocationManager.locationServicesEnabled() | |
} | |
) | |
} | |
private class LocationDelegate: NSObject, CLLocationManagerDelegate { | |
let continuation: AsyncStream<Action>.Continuation | |
init(continuation: AsyncStream<Action>.Continuation) { | |
self.continuation = continuation | |
super.init() | |
} | |
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { | |
continuation.yield(.didChangeAuthorization(manager.authorizationStatus)) | |
} | |
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { | |
continuation.yield(.didUpdateLocations(locations)) | |
} | |
} | |
} | |
extension DependencyValues { | |
var locationManager: LocationManagerClient { | |
get { self[LocationManagerClient.self] } | |
set { self[LocationManagerClient.self] = newValue } | |
} | |
} | |
// MARK: - LocationPermissionView | |
struct LocationPermissionView: View { | |
let store: StoreOf<LocationPermissionFeature> | |
var body: some View { | |
WithViewStore(store, observe: { $0 }) { viewStore in | |
VStack { | |
Text("Location Status: \(viewStore.authorizationStatus.description)") | |
.padding() | |
if let location = viewStore.currentLocation { | |
Text("Current Location:") | |
.padding(.top) | |
Text("Latitude: \(location.coordinate.latitude)") | |
Text("Longitude: \(location.coordinate.longitude)") | |
} | |
Button("Request Location") { | |
viewStore.send(.requestLocationPermission) | |
} | |
.padding() | |
} | |
.onAppear { | |
viewStore.send(.onAppear) | |
} | |
.onChange(of: viewStore.authorizationStatus) { olVal, newVal in | |
if olVal != newVal { | |
viewStore.send(.locationManager(.didChangeAuthorization(newVal))) | |
} | |
} | |
.alert( | |
viewStore.locationAlertTitle, | |
isPresented: viewStore.binding( | |
get: \.showLocationAlert, | |
send: { _ in .dismissLocationAlert } | |
) | |
) { | |
Button("Open Settings") { | |
viewStore.send(.openSettings) | |
} | |
Button("Cancel", role: .cancel) { | |
viewStore.send(.dismissLocationAlert) | |
} | |
} message: { | |
Text(viewStore.locationAlertMessage) | |
} | |
} | |
} | |
} | |
// MARK: - Helper Extension | |
extension CLAuthorizationStatus: CustomStringConvertible { | |
public var description: String { | |
switch self { | |
case .notDetermined: return "Not Determined" | |
case .restricted: return "Restricted" | |
case .denied: return "Denied" | |
case .authorizedAlways: return "Always Authorized" | |
case .authorizedWhenInUse: return "When In Use" | |
@unknown default: return "Unknown" | |
} | |
} | |
} | |
@main | |
struct TCAApp: App { | |
var body: some Scene { | |
WindowGroup { | |
LocationPermissionView( | |
store: Store( | |
initialState: LocationPermissionFeature.State() | |
) { | |
LocationPermissionFeature() | |
} | |
) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment