FieldLocationCheckIn.swift (6965B)
1 import Foundation 2 import RadrootsKit 3 4 public struct FieldLocationCheckInReading: Equatable, Sendable { 5 public let latitude: Double 6 public let longitude: Double 7 public let horizontalAccuracyMeters: Double 8 public let capturedAt: Date 9 10 init(reading: RadrootsLocationReading) { 11 self.latitude = reading.coordinate.latitude 12 self.longitude = reading.coordinate.longitude 13 self.horizontalAccuracyMeters = reading.horizontalAccuracyMeters 14 self.capturedAt = reading.capturedAt 15 } 16 17 public var coordinateSummary: String { 18 String(format: "%.4f, %.4f", latitude, longitude) 19 } 20 21 public var accuracySummary: String { 22 String(format: "within %.0f m", horizontalAccuracyMeters) 23 } 24 } 25 26 public enum FieldLocationCheckInState: Equatable, Sendable { 27 case idle(RadrootsLocationServicesAvailability) 28 case checking(RadrootsLocationServicesAvailability) 29 case checkedIn(FieldLocationCheckInReading) 30 case failed(RadrootsLocationServicesAvailability?, String) 31 32 public var availability: RadrootsLocationServicesAvailability? { 33 switch self { 34 case .idle(let availability): 35 availability 36 case .checking(let availability): 37 availability 38 case .checkedIn: 39 nil 40 case .failed(let availability, _): 41 availability 42 } 43 } 44 } 45 46 public struct FieldLocationCheckIn: Sendable { 47 private let locationServices: any RadrootsLocationServices 48 private let request: RadrootsCurrentLocationRequest 49 50 public init( 51 locationServices: any RadrootsLocationServices = RadrootsAppleLocationServices(), 52 request: RadrootsCurrentLocationRequest? = nil 53 ) { 54 self.locationServices = locationServices 55 self.request = request ?? Self.defaultRequest() 56 } 57 58 static func configured() -> Self { 59 guard let mode = FieldLocationCheckInUITestMode.current else { 60 return Self() 61 } 62 return Self(locationServices: FieldLocationCheckInUITestLocationServices(mode: mode)) 63 } 64 65 public func status() async -> FieldLocationCheckInState { 66 .idle(await locationServices.currentAvailability()) 67 } 68 69 public func checkIn() async -> FieldLocationCheckInState { 70 let availability = await locationServices.currentAvailability() 71 guard availability.locationServicesEnabled else { 72 return .failed(availability, "Location Services are disabled.") 73 } 74 do { 75 if availability.authorization == .notDetermined { 76 let authorization = try await locationServices.requestWhenInUseAuthorization() 77 guard authorization == .authorizedWhenInUse || authorization == .authorizedAlways else { 78 return .failed( 79 RadrootsLocationServicesAvailability( 80 locationServicesEnabled: true, 81 authorization: authorization 82 ), 83 "Location permission was not granted." 84 ) 85 } 86 } 87 let result = try await locationServices.currentLocation(request) 88 return .checkedIn(FieldLocationCheckInReading(reading: result.reading)) 89 } catch RadrootsLocationServicesError.permissionDenied(let message) { 90 return .failed(availability, message) 91 } catch RadrootsLocationServicesError.unavailable(let message) { 92 return .failed(availability, message) 93 } catch RadrootsLocationServicesError.timeout(let message) { 94 return .failed(availability, message) 95 } catch RadrootsLocationServicesError.cancelled(let message) { 96 return .failed(availability, message) 97 } catch { 98 return .failed(availability, error.fieldRuntimeMessage) 99 } 100 } 101 102 private static func defaultRequest() -> RadrootsCurrentLocationRequest { 103 do { 104 return try RadrootsCurrentLocationRequest( 105 timeoutSeconds: 10, 106 desiredAccuracyMeters: 100, 107 maximumCachedReadingAgeSeconds: 30 108 ) 109 } catch { 110 preconditionFailure("invalid default location check-in request") 111 } 112 } 113 } 114 115 private enum FieldLocationCheckInUITestMode: String { 116 case success 117 case denied 118 case unavailable 119 case timeout 120 121 static var current: Self? { 122 guard ProcessInfo.processInfo.environment["RADROOTS_FIELD_IOS_UI_TEST"] == "true" else { 123 return nil 124 } 125 guard let raw = ProcessInfo.processInfo.environment["RADROOTS_FIELD_IOS_UI_TEST_LOCATION_MODE"] else { 126 return nil 127 } 128 return Self(rawValue: raw) 129 } 130 } 131 132 private actor FieldLocationCheckInUITestLocationServices: RadrootsLocationServices { 133 private let mode: FieldLocationCheckInUITestMode 134 135 init(mode: FieldLocationCheckInUITestMode) { 136 self.mode = mode 137 } 138 139 func currentAvailability() async -> RadrootsLocationServicesAvailability { 140 switch mode { 141 case .success: 142 RadrootsLocationServicesAvailability(locationServicesEnabled: true, authorization: .authorizedWhenInUse) 143 case .denied: 144 RadrootsLocationServicesAvailability(locationServicesEnabled: true, authorization: .denied) 145 case .unavailable: 146 RadrootsLocationServicesAvailability(locationServicesEnabled: false, authorization: .unavailable) 147 case .timeout: 148 RadrootsLocationServicesAvailability(locationServicesEnabled: true, authorization: .authorizedWhenInUse) 149 } 150 } 151 152 func requestWhenInUseAuthorization() async throws -> RadrootsLocationAuthorization { 153 switch mode { 154 case .success, .timeout: 155 .authorizedWhenInUse 156 case .denied: 157 throw RadrootsLocationServicesError.permissionDenied("location permission is denied") 158 case .unavailable: 159 throw RadrootsLocationServicesError.unavailable("location services are unavailable") 160 } 161 } 162 163 func currentLocation(_ request: RadrootsCurrentLocationRequest) async throws -> RadrootsCurrentLocationResult { 164 switch mode { 165 case .success: 166 let reading = try RadrootsLocationReading( 167 coordinate: RadrootsLocationCoordinate(latitude: 49.2827, longitude: -123.1207), 168 horizontalAccuracyMeters: 12, 169 capturedAt: Date() 170 ) 171 return try RadrootsCurrentLocationResult(reading: reading, authorization: .authorizedWhenInUse) 172 case .denied: 173 throw RadrootsLocationServicesError.permissionDenied("location permission is denied") 174 case .unavailable: 175 throw RadrootsLocationServicesError.unavailable("location services are unavailable") 176 case .timeout: 177 throw RadrootsLocationServicesError.timeout("current location request timed out") 178 } 179 } 180 }