field_ios

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

commit dac6052952c6fd09b33371adb2018fbd03df9b23
parent 5bfff42bbaaf0d653c296daef7aed641fdf3a3dc
Author: triesap <tyson@radroots.org>
Date:   Sun, 14 Jun 2026 03:37:49 -0700

ui: add visible location check-in flow

- replace placeholder location rows with a real check-in control
- surface location permission readiness and failure states
- show app-local coordinate and accuracy summaries after check-in
- add stable accessibility identifiers for the flow

Diffstat:
MRadroots/Views/HomeView.swift | 141+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 139 insertions(+), 2 deletions(-)

diff --git a/Radroots/Views/HomeView.swift b/Radroots/Views/HomeView.swift @@ -1,3 +1,4 @@ +import RadrootsKit import SwiftUI private enum HomeTab: String, Hashable { @@ -67,7 +68,7 @@ private struct TodayView: View { Section("Next Actions") { FieldActionRow(title: "Photo Evidence", subtitle: "Document a crop, delivery, or field condition.", systemImage: "camera.fill") - FieldActionRow(title: "Location Check-in", subtitle: "Record where field work is happening.", systemImage: "location.fill") + LocationCheckInRow() FieldActionRow(title: "Status Log", subtitle: "Capture a short operational update.", systemImage: "text.badge.checkmark") FieldActionRow(title: "Compliance Note", subtitle: "Reserve audit-ready notes for the current workflow.", systemImage: "checkmark.seal.fill") } @@ -97,11 +98,13 @@ private struct TodayView: View { } private struct CaptureView: View { + @EnvironmentObject private var app: AppState + var body: some View { List { Section("Capture") { FieldActionRow(title: "Photo Evidence", subtitle: "Attach visual proof to field work.", systemImage: "camera.fill") - FieldActionRow(title: "Location Check-in", subtitle: "Pair a note with the current site.", systemImage: "location.fill") + LocationCheckInRow() FieldActionRow(title: "Status Log", subtitle: "Record observations from the field.", systemImage: "square.and.pencil") FieldActionRow(title: "Compliance Note", subtitle: "Prepare traceability notes for review.", systemImage: "checkmark.seal.fill") } @@ -158,6 +161,140 @@ private struct FieldActionRow: View { } } +private struct LocationCheckInRow: View { + @EnvironmentObject private var app: AppState + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + HStack(alignment: .top, spacing: 14) { + Image(systemName: systemImage) + .font(.title3.weight(.semibold)) + .foregroundStyle(systemColor) + .frame(width: 34, height: 34) + .accessibilityHidden(true) + VStack(alignment: .leading, spacing: 4) { + Text("Location Check-in") + .font(.headline) + Text(statusText) + .font(.subheadline) + .foregroundStyle(.secondary) + .accessibilityIdentifier("field_ios.location_check_in.status") + if let detailText { + Text(detailText) + .font(.footnote) + .foregroundStyle(.secondary) + .accessibilityIdentifier("field_ios.location_check_in.detail") + } + } + Spacer() + if isChecking { + ProgressView() + .accessibilityIdentifier("field_ios.location_check_in.progress") + } + } + + Button { + Task { + await app.performLocationCheckIn() + } + } label: { + Label(actionTitle, systemImage: "location.fill") + .font(.subheadline.weight(.semibold)) + .frame(maxWidth: .infinity) + } + .buttonStyle(.borderedProminent) + .disabled(isChecking) + .accessibilityIdentifier("field_ios.location_check_in.action") + } + .padding(.vertical, 4) + .accessibilityIdentifier("field_ios.location_check_in.card") + .task { + await app.refreshLocationCheckInStatus() + } + } + + private var isChecking: Bool { + if case .checking = app.locationCheckInState { + return true + } + return false + } + + private var systemImage: String { + switch app.locationCheckInState { + case .checkedIn: + "location.circle.fill" + case .failed: + "location.slash.fill" + case .checking: + "location.fill" + case .idle(let availability): + availability.canRequestCurrentLocation ? "location.circle.fill" : "location.fill" + } + } + + private var systemColor: Color { + switch app.locationCheckInState { + case .checkedIn: + .green + case .failed: + .red + case .checking: + .green + case .idle(let availability): + availability.canRequestCurrentLocation ? .green : .secondary + } + } + + private var actionTitle: String { + isChecking ? "Checking In" : "Check In" + } + + private var statusText: String { + switch app.locationCheckInState { + case .idle(let availability): + statusText(for: availability) + case .checking: + "Checking current location..." + case .checkedIn(let reading): + "Checked in at \(reading.coordinateSummary)" + case .failed(_, let message): + "Check-in unavailable: \(message)" + } + } + + private var detailText: String? { + switch app.locationCheckInState { + case .checkedIn(let reading): + reading.accuracySummary + case .idle(let availability): + availability.authorization == .notDetermined ? "Permission will be requested when you check in." : nil + case .checking, .failed: + nil + } + } + + private func statusText(for availability: RadrootsLocationServicesAvailability) -> String { + guard availability.locationServicesEnabled else { + return "Location Services are disabled." + } + switch availability.authorization { + case .notDetermined: + return "Ready to request location permission." + case .authorizedWhenInUse, .authorizedAlways: + return "Ready to record the current site." + case .denied: + return "Location permission is denied." + case .restricted: + return "Location permission is restricted." + case .unavailable: + return "Location Services are unavailable." + case .unsupported: + return "Location Services are unsupported." + } + } +} + private struct ActivityRow: View { let title: String let detail: String