field_ios

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

commit 974e7375deb4b519dc7fc931aa7d731a179adff5
parent dac6052952c6fd09b33371adb2018fbd03df9b23
Author: triesap <tyson@radroots.org>
Date:   Sun, 14 Jun 2026 04:04:13 -0700

app: add location check-in test probes

- add deterministic UI-test location service modes
- route AppState through the configured check-in runtime
- keep production location behavior on AppleKit services
- support success denied unavailable and timeout UI proofs

Diffstat:
MRadroots/App/AppState.swift | 2+-
MRadroots/Runtime/FieldLocationCheckIn.swift | 94++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
2 files changed, 89 insertions(+), 7 deletions(-)

diff --git a/Radroots/App/AppState.swift b/Radroots/App/AppState.swift @@ -76,7 +76,7 @@ public final class AppState: ObservableObject { private var statusTask: Task<Void, Never>? private var secureIdentityStore: FieldSecureIdentityStore? private var identityMetadataStore: FieldIdentityPublicMetadataStore? - private let locationCheckIn = FieldLocationCheckIn() + private let locationCheckIn = FieldLocationCheckIn.configured() public init(radroots: Radroots = Radroots()) { self.radroots = radroots diff --git a/Radroots/Runtime/FieldLocationCheckIn.swift b/Radroots/Runtime/FieldLocationCheckIn.swift @@ -49,14 +49,17 @@ public struct FieldLocationCheckIn: Sendable { public init( locationServices: any RadrootsLocationServices = RadrootsAppleLocationServices(), - request: RadrootsCurrentLocationRequest = try! RadrootsCurrentLocationRequest( - timeoutSeconds: 10, - desiredAccuracyMeters: 100, - maximumCachedReadingAgeSeconds: 30 - ) + request: RadrootsCurrentLocationRequest? = nil ) { self.locationServices = locationServices - self.request = request + self.request = request ?? Self.defaultRequest() + } + + static func configured() -> Self { + guard let mode = FieldLocationCheckInUITestMode.current else { + return Self() + } + return Self(locationServices: FieldLocationCheckInUITestLocationServices(mode: mode)) } public func status() async -> FieldLocationCheckInState { @@ -95,4 +98,83 @@ public struct FieldLocationCheckIn: Sendable { return .failed(availability, error.localizedDescription) } } + + private static func defaultRequest() -> RadrootsCurrentLocationRequest { + do { + return try RadrootsCurrentLocationRequest( + timeoutSeconds: 10, + desiredAccuracyMeters: 100, + maximumCachedReadingAgeSeconds: 30 + ) + } catch { + preconditionFailure("invalid default location check-in request") + } + } +} + +private enum FieldLocationCheckInUITestMode: String { + case success + case denied + case unavailable + case timeout + + static var current: Self? { + guard ProcessInfo.processInfo.environment["RADROOTS_FIELD_IOS_UI_TEST"] == "true" else { + return nil + } + guard let raw = ProcessInfo.processInfo.environment["RADROOTS_FIELD_IOS_UI_TEST_LOCATION_MODE"] else { + return nil + } + return Self(rawValue: raw) + } +} + +private actor FieldLocationCheckInUITestLocationServices: RadrootsLocationServices { + private let mode: FieldLocationCheckInUITestMode + + init(mode: FieldLocationCheckInUITestMode) { + self.mode = mode + } + + func currentAvailability() async -> RadrootsLocationServicesAvailability { + switch mode { + case .success: + RadrootsLocationServicesAvailability(locationServicesEnabled: true, authorization: .authorizedWhenInUse) + case .denied: + RadrootsLocationServicesAvailability(locationServicesEnabled: true, authorization: .denied) + case .unavailable: + RadrootsLocationServicesAvailability(locationServicesEnabled: false, authorization: .unavailable) + case .timeout: + RadrootsLocationServicesAvailability(locationServicesEnabled: true, authorization: .authorizedWhenInUse) + } + } + + func requestWhenInUseAuthorization() async throws -> RadrootsLocationAuthorization { + switch mode { + case .success, .timeout: + .authorizedWhenInUse + case .denied: + throw RadrootsLocationServicesError.permissionDenied("location permission is denied") + case .unavailable: + throw RadrootsLocationServicesError.unavailable("location services are unavailable") + } + } + + func currentLocation(_ request: RadrootsCurrentLocationRequest) async throws -> RadrootsCurrentLocationResult { + switch mode { + case .success: + let reading = try RadrootsLocationReading( + coordinate: RadrootsLocationCoordinate(latitude: 49.2827, longitude: -123.1207), + horizontalAccuracyMeters: 12, + capturedAt: Date() + ) + return try RadrootsCurrentLocationResult(reading: reading, authorization: .authorizedWhenInUse) + case .denied: + throw RadrootsLocationServicesError.permissionDenied("location permission is denied") + case .unavailable: + throw RadrootsLocationServicesError.unavailable("location services are unavailable") + case .timeout: + throw RadrootsLocationServicesError.timeout("current location request timed out") + } + } }