Skip to content

Instantly share code, notes, and snippets.

@emrdgrmnci
Last active December 18, 2024 19:56
Show Gist options
  • Save emrdgrmnci/2f01bf5afca63c981a731f503d808e8b to your computer and use it in GitHub Desktop.
Save emrdgrmnci/2f01bf5afca63c981a731f503d808e8b to your computer and use it in GitHub Desktop.
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