apple_kit

Apple-native services for Radroots iOS and macOS apps
git clone https://radroots.dev/git/apple_kit.git
Log | Files | Refs | README | LICENSE

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 }