RadrootsPermissionLocation.swift (9698B)
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 40 -> [RadrootsPermissionSnapshot] 41 { 42 var snapshots: [RadrootsPermissionSnapshot] = [] 43 snapshots.reserveCapacity(kinds.count) 44 for kind in kinds { 45 try await snapshots.append(snapshot(for: kind)) 46 } 47 return snapshots 48 } 49 } 50 51 public enum RadrootsLocationAuthorization: String, Sendable, Equatable, Hashable { 52 case notDetermined 53 case authorizedWhenInUse 54 case authorizedAlways 55 case denied 56 case restricted 57 case unavailable 58 case unsupported 59 60 public var permissionStatus: RadrootsPermissionStatus { 61 switch self { 62 case .notDetermined: 63 .notDetermined 64 case .authorizedWhenInUse: 65 .authorized 66 case .authorizedAlways: 67 .authorized 68 case .denied: 69 .denied 70 case .restricted: 71 .restricted 72 case .unavailable: 73 .unavailable 74 case .unsupported: 75 .unsupported 76 } 77 } 78 } 79 80 public struct RadrootsLocationServicesAvailability: Sendable, Equatable, Hashable { 81 public let locationServicesEnabled: Bool 82 public let authorization: RadrootsLocationAuthorization 83 84 public init(locationServicesEnabled: Bool, authorization: RadrootsLocationAuthorization) { 85 self.locationServicesEnabled = locationServicesEnabled 86 self.authorization = authorization 87 } 88 89 public var canRequestWhenInUseAuthorization: Bool { 90 locationServicesEnabled && authorization == .notDetermined 91 } 92 93 public var canRequestCurrentLocation: Bool { 94 locationServicesEnabled 95 && (authorization == .authorizedWhenInUse || authorization == .authorizedAlways) 96 } 97 } 98 99 public struct RadrootsLocationCoordinate: Sendable, Equatable, Hashable { 100 public let latitude: Double 101 public let longitude: Double 102 103 public init(latitude: Double, longitude: Double) throws { 104 guard latitude.isFinite, (-90.0 ... 90.0).contains(latitude) else { 105 throw RadrootsLocationServicesError.invalidRequest 106 } 107 guard longitude.isFinite, (-180.0 ... 180.0).contains(longitude) else { 108 throw RadrootsLocationServicesError.invalidRequest 109 } 110 self.latitude = latitude 111 self.longitude = longitude 112 } 113 } 114 115 public struct RadrootsLocationReading: Sendable, Equatable, Hashable { 116 public let coordinate: RadrootsLocationCoordinate 117 public let horizontalAccuracyMeters: Double 118 public let altitudeMeters: Double? 119 public let verticalAccuracyMeters: Double? 120 public let speedMetersPerSecond: Double? 121 public let courseDegrees: Double? 122 public let capturedAt: Date 123 124 public init( 125 coordinate: RadrootsLocationCoordinate, 126 horizontalAccuracyMeters: Double, 127 altitudeMeters: Double? = nil, 128 verticalAccuracyMeters: Double? = nil, 129 speedMetersPerSecond: Double? = nil, 130 courseDegrees: Double? = nil, 131 capturedAt: Date 132 ) throws { 133 self.coordinate = coordinate 134 self.horizontalAccuracyMeters = try Self.normalizedNonNegativeFinite( 135 horizontalAccuracyMeters, 136 field: "horizontal accuracy" 137 ) 138 self.altitudeMeters = try Self.normalizedOptionalFinite(altitudeMeters, field: "altitude") 139 self.verticalAccuracyMeters = try Self.normalizedOptionalNonNegativeFinite( 140 verticalAccuracyMeters, 141 field: "vertical accuracy" 142 ) 143 self.speedMetersPerSecond = try Self.normalizedOptionalNonNegativeFinite( 144 speedMetersPerSecond, 145 field: "speed" 146 ) 147 self.courseDegrees = try Self.normalizedOptionalCourse(courseDegrees) 148 guard capturedAt.timeIntervalSinceReferenceDate.isFinite else { 149 throw RadrootsLocationServicesError.invalidRequest 150 } 151 self.capturedAt = capturedAt 152 } 153 154 public func age(relativeTo now: Date) throws -> TimeInterval { 155 guard now.timeIntervalSinceReferenceDate.isFinite else { 156 throw RadrootsLocationServicesError.invalidRequest 157 } 158 return now.timeIntervalSince(capturedAt) 159 } 160 161 public func isFresh(relativeTo now: Date, maximumAgeSeconds: TimeInterval) throws -> Bool { 162 let normalizedMaximumAge = try Self.normalizedNonNegativeFinite( 163 maximumAgeSeconds, field: "maximum age") 164 let currentAge = try age(relativeTo: now) 165 return currentAge >= 0 && currentAge <= normalizedMaximumAge 166 } 167 168 public static func normalizedNonNegativeFinite(_ value: Double, field: String) throws -> Double { 169 guard value.isFinite, value >= 0 else { 170 throw RadrootsLocationServicesError.invalidRequest 171 } 172 return value 173 } 174 175 public static func normalizedOptionalFinite(_ value: Double?, field: String) throws -> Double? { 176 guard let value else { 177 return nil 178 } 179 guard value.isFinite else { 180 throw RadrootsLocationServicesError.invalidRequest 181 } 182 return value 183 } 184 185 public static func normalizedOptionalNonNegativeFinite(_ value: Double?, field: String) throws 186 -> Double? 187 { 188 guard let value else { 189 return nil 190 } 191 return try normalizedNonNegativeFinite(value, field: field) 192 } 193 194 public static func normalizedOptionalCourse(_ value: Double?) throws -> Double? { 195 guard let value else { 196 return nil 197 } 198 guard value.isFinite, (0.0 ..< 360.0).contains(value) else { 199 throw RadrootsLocationServicesError.invalidRequest 200 } 201 return value 202 } 203 } 204 205 public struct RadrootsCurrentLocationRequest: Sendable, Equatable, Hashable { 206 public let timeoutSeconds: TimeInterval 207 public let desiredAccuracyMeters: Double? 208 public let maximumCachedReadingAgeSeconds: TimeInterval? 209 210 public init( 211 timeoutSeconds: TimeInterval = 12, 212 desiredAccuracyMeters: Double? = nil, 213 maximumCachedReadingAgeSeconds: TimeInterval? = nil 214 ) throws { 215 guard timeoutSeconds.isFinite, timeoutSeconds > 0 else { 216 throw RadrootsLocationServicesError.invalidRequest 217 } 218 self.timeoutSeconds = timeoutSeconds 219 self.desiredAccuracyMeters = try RadrootsLocationReading.normalizedOptionalNonNegativeFinite( 220 desiredAccuracyMeters, 221 field: "desired accuracy" 222 ) 223 self.maximumCachedReadingAgeSeconds = 224 try RadrootsLocationReading.normalizedOptionalNonNegativeFinite( 225 maximumCachedReadingAgeSeconds, 226 field: "maximum cached reading age" 227 ) 228 } 229 } 230 231 public struct RadrootsCurrentLocationResult: Sendable, Equatable, Hashable { 232 public let reading: RadrootsLocationReading 233 public let authorization: RadrootsLocationAuthorization 234 public let servedFromCache: Bool 235 236 public init( 237 reading: RadrootsLocationReading, 238 authorization: RadrootsLocationAuthorization, 239 servedFromCache: Bool = false 240 ) throws { 241 guard authorization == .authorizedWhenInUse || authorization == .authorizedAlways else { 242 throw RadrootsLocationServicesError.invalidRequest 243 } 244 self.reading = reading 245 self.authorization = authorization 246 self.servedFromCache = servedFromCache 247 } 248 } 249 250 public enum RadrootsLocationServicesError: Error, Equatable, Sendable { 251 case invalidRequest 252 case permissionDenied 253 case unavailable 254 case timeout 255 case cancelled 256 case transientFailure 257 case permanentFailure 258 } 259 260 extension RadrootsLocationServicesError: LocalizedError { 261 public var errorDescription: String? { 262 switch self { 263 case .invalidRequest: "The location request is invalid." 264 case .permissionDenied: "Location permission was denied." 265 case .unavailable: "Location services are unavailable." 266 case .timeout: "The location request timed out." 267 case .cancelled: "The location request was cancelled." 268 case .transientFailure: "Location could not be determined temporarily." 269 case .permanentFailure: "Location could not be determined." 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 278 -> RadrootsCurrentLocationResult 279 }