RadrootsPermissionLocation.swift (10044B)
1 import Foundation 2 3 public enum RadrootsPermissionKind: String, Sendable, Equatable, Hashable, CaseIterable { 4 case notifications 5 case camera 6 case photos 7 case microphone 8 case location 9 } 10 11 public enum RadrootsPermissionStatus: String, Sendable, Equatable, Hashable { 12 case notDetermined 13 case authorized 14 case denied 15 case restricted 16 case limited 17 case unavailable 18 case unsupported 19 } 20 21 public struct RadrootsPermissionSnapshot: Sendable, Equatable, Hashable { 22 public let kind: RadrootsPermissionKind 23 public let status: RadrootsPermissionStatus 24 public let observedAt: Date 25 26 public init(kind: RadrootsPermissionKind, status: RadrootsPermissionStatus, observedAt: Date) { 27 self.kind = kind 28 self.status = status 29 self.observedAt = observedAt 30 } 31 } 32 33 public protocol RadrootsPermissionStatusProvider: Sendable { 34 func snapshot(for kind: RadrootsPermissionKind) async throws -> RadrootsPermissionSnapshot 35 func snapshots(for kinds: [RadrootsPermissionKind]) async throws -> [RadrootsPermissionSnapshot] 36 } 37 38 extension RadrootsPermissionStatusProvider { 39 public func snapshots(for kinds: [RadrootsPermissionKind]) async throws -> [RadrootsPermissionSnapshot] { 40 var snapshots: [RadrootsPermissionSnapshot] = [] 41 snapshots.reserveCapacity(kinds.count) 42 for kind in kinds { 43 snapshots.append(try await snapshot(for: kind)) 44 } 45 return snapshots 46 } 47 } 48 49 public enum RadrootsLocationAuthorization: String, Sendable, Equatable, Hashable { 50 case notDetermined 51 case authorizedWhenInUse 52 case authorizedAlways 53 case denied 54 case restricted 55 case unavailable 56 case unsupported 57 58 public var permissionStatus: RadrootsPermissionStatus { 59 switch self { 60 case .notDetermined: 61 .notDetermined 62 case .authorizedWhenInUse: 63 .authorized 64 case .authorizedAlways: 65 .authorized 66 case .denied: 67 .denied 68 case .restricted: 69 .restricted 70 case .unavailable: 71 .unavailable 72 case .unsupported: 73 .unsupported 74 } 75 } 76 } 77 78 public struct RadrootsLocationServicesAvailability: Sendable, Equatable, Hashable { 79 public let locationServicesEnabled: Bool 80 public let authorization: RadrootsLocationAuthorization 81 82 public init(locationServicesEnabled: Bool, authorization: RadrootsLocationAuthorization) { 83 self.locationServicesEnabled = locationServicesEnabled 84 self.authorization = authorization 85 } 86 87 public var canRequestWhenInUseAuthorization: Bool { 88 locationServicesEnabled && authorization == .notDetermined 89 } 90 91 public var canRequestCurrentLocation: Bool { 92 locationServicesEnabled && (authorization == .authorizedWhenInUse || authorization == .authorizedAlways) 93 } 94 } 95 96 public struct RadrootsLocationCoordinate: Sendable, Equatable, Hashable { 97 public let latitude: Double 98 public let longitude: Double 99 100 public init(latitude: Double, longitude: Double) throws { 101 guard latitude.isFinite, (-90.0...90.0).contains(latitude) else { 102 throw RadrootsLocationServicesError.invalidRequest("latitude must be between -90 and 90") 103 } 104 guard longitude.isFinite, (-180.0...180.0).contains(longitude) else { 105 throw RadrootsLocationServicesError.invalidRequest("longitude must be between -180 and 180") 106 } 107 self.latitude = latitude 108 self.longitude = longitude 109 } 110 } 111 112 public struct RadrootsLocationReading: Sendable, Equatable, Hashable { 113 public let coordinate: RadrootsLocationCoordinate 114 public let horizontalAccuracyMeters: Double 115 public let altitudeMeters: Double? 116 public let verticalAccuracyMeters: Double? 117 public let speedMetersPerSecond: Double? 118 public let courseDegrees: Double? 119 public let capturedAt: Date 120 121 public init( 122 coordinate: RadrootsLocationCoordinate, 123 horizontalAccuracyMeters: Double, 124 altitudeMeters: Double? = nil, 125 verticalAccuracyMeters: Double? = nil, 126 speedMetersPerSecond: Double? = nil, 127 courseDegrees: Double? = nil, 128 capturedAt: Date 129 ) throws { 130 self.coordinate = coordinate 131 self.horizontalAccuracyMeters = try Self.normalizedNonNegativeFinite( 132 horizontalAccuracyMeters, 133 field: "horizontal accuracy" 134 ) 135 self.altitudeMeters = try Self.normalizedOptionalFinite(altitudeMeters, field: "altitude") 136 self.verticalAccuracyMeters = try Self.normalizedOptionalNonNegativeFinite( 137 verticalAccuracyMeters, 138 field: "vertical accuracy" 139 ) 140 self.speedMetersPerSecond = try Self.normalizedOptionalNonNegativeFinite( 141 speedMetersPerSecond, 142 field: "speed" 143 ) 144 self.courseDegrees = try Self.normalizedOptionalCourse(courseDegrees) 145 guard capturedAt.timeIntervalSinceReferenceDate.isFinite else { 146 throw RadrootsLocationServicesError.invalidRequest("captured timestamp must be finite") 147 } 148 self.capturedAt = capturedAt 149 } 150 151 public func age(relativeTo now: Date) throws -> TimeInterval { 152 guard now.timeIntervalSinceReferenceDate.isFinite else { 153 throw RadrootsLocationServicesError.invalidRequest("comparison timestamp must be finite") 154 } 155 return now.timeIntervalSince(capturedAt) 156 } 157 158 public func isFresh(relativeTo now: Date, maximumAgeSeconds: TimeInterval) throws -> Bool { 159 let normalizedMaximumAge = try Self.normalizedNonNegativeFinite(maximumAgeSeconds, field: "maximum age") 160 let currentAge = try age(relativeTo: now) 161 return currentAge >= 0 && currentAge <= normalizedMaximumAge 162 } 163 164 public static func normalizedNonNegativeFinite(_ value: Double, field: String) throws -> Double { 165 guard value.isFinite, value >= 0 else { 166 throw RadrootsLocationServicesError.invalidRequest("\(field) must be finite and non-negative") 167 } 168 return value 169 } 170 171 public static func normalizedOptionalFinite(_ value: Double?, field: String) throws -> Double? { 172 guard let value else { 173 return nil 174 } 175 guard value.isFinite else { 176 throw RadrootsLocationServicesError.invalidRequest("\(field) must be finite") 177 } 178 return value 179 } 180 181 public static func normalizedOptionalNonNegativeFinite(_ value: Double?, field: String) throws -> Double? { 182 guard let value else { 183 return nil 184 } 185 return try normalizedNonNegativeFinite(value, field: field) 186 } 187 188 public static func normalizedOptionalCourse(_ value: Double?) throws -> Double? { 189 guard let value else { 190 return nil 191 } 192 guard value.isFinite, (0.0..<360.0).contains(value) else { 193 throw RadrootsLocationServicesError.invalidRequest("course must be between 0 and 359.999 degrees") 194 } 195 return value 196 } 197 } 198 199 public struct RadrootsCurrentLocationRequest: Sendable, Equatable, Hashable { 200 public let timeoutSeconds: TimeInterval 201 public let desiredAccuracyMeters: Double? 202 public let maximumCachedReadingAgeSeconds: TimeInterval? 203 204 public init( 205 timeoutSeconds: TimeInterval = 12, 206 desiredAccuracyMeters: Double? = nil, 207 maximumCachedReadingAgeSeconds: TimeInterval? = nil 208 ) throws { 209 guard timeoutSeconds.isFinite, timeoutSeconds > 0 else { 210 throw RadrootsLocationServicesError.invalidRequest("location timeout must be finite and greater than zero") 211 } 212 self.timeoutSeconds = timeoutSeconds 213 self.desiredAccuracyMeters = try RadrootsLocationReading.normalizedOptionalNonNegativeFinite( 214 desiredAccuracyMeters, 215 field: "desired accuracy" 216 ) 217 self.maximumCachedReadingAgeSeconds = try RadrootsLocationReading.normalizedOptionalNonNegativeFinite( 218 maximumCachedReadingAgeSeconds, 219 field: "maximum cached reading age" 220 ) 221 } 222 } 223 224 public struct RadrootsCurrentLocationResult: Sendable, Equatable, Hashable { 225 public let reading: RadrootsLocationReading 226 public let authorization: RadrootsLocationAuthorization 227 public let servedFromCache: Bool 228 229 public init( 230 reading: RadrootsLocationReading, 231 authorization: RadrootsLocationAuthorization, 232 servedFromCache: Bool = false 233 ) throws { 234 guard authorization == .authorizedWhenInUse || authorization == .authorizedAlways else { 235 throw RadrootsLocationServicesError.invalidRequest("current location result requires authorized location access") 236 } 237 self.reading = reading 238 self.authorization = authorization 239 self.servedFromCache = servedFromCache 240 } 241 } 242 243 public enum RadrootsLocationServicesError: Error, Equatable, Sendable { 244 case invalidRequest(String) 245 case permissionDenied(String) 246 case unavailable(String) 247 case timeout(String) 248 case cancelled(String) 249 case transientFailure(String) 250 case permanentFailure(String) 251 } 252 253 extension RadrootsLocationServicesError: LocalizedError { 254 public var errorDescription: String? { 255 switch self { 256 case .invalidRequest(let message): 257 message 258 case .permissionDenied(let message): 259 message 260 case .unavailable(let message): 261 message 262 case .timeout(let message): 263 message 264 case .cancelled(let message): 265 message 266 case .transientFailure(let message): 267 message 268 case .permanentFailure(let message): 269 message 270 } 271 } 272 } 273 274 public protocol RadrootsLocationServices: Sendable { 275 func currentAvailability() async -> RadrootsLocationServicesAvailability 276 func requestWhenInUseAuthorization() async throws -> RadrootsLocationAuthorization 277 func currentLocation(_ request: RadrootsCurrentLocationRequest) async throws -> RadrootsCurrentLocationResult 278 }