field_ios

In-the-field app for Radroots on iOS
git clone https://radroots.dev/git/field_ios.git
Log | Files | Refs | LICENSE

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 }