field_ios

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

commit a954168d99cc6fbca4261212012d1b4ede6dd23d
parent 834ad56bfb220169bcdaba367eaaa781a0cf46e9
Author: triesap <tyson@radroots.org>
Date:   Thu, 11 Jun 2026 21:58:01 -0700

ui: align field shell tabs

Replace the retired feed/market tab scaffold with the approved Today, Capture, Activity, and Settings field shell posture.

Keep runtime diagnostics visible on Today while reserving field workflow surfaces for capture and activity.

Diffstat:
MRadroots/Views/HomeView.swift | 196++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
1 file changed, 185 insertions(+), 11 deletions(-)

diff --git a/Radroots/Views/HomeView.swift b/Radroots/Views/HomeView.swift @@ -1,34 +1,208 @@ import SwiftUI -private enum HomeTab: Hashable { - case feed - case market +private enum HomeTab: String, Hashable { + case today + case capture + case activity case settings } struct HomeView: View { - @State private var selection: HomeTab = .feed + @AppStorage("field_ios.selected_tab") private var selection = HomeTab.today.rawValue var body: some View { TabView(selection: $selection) { NavigationStack { - PostFeedView() + TodayView() } - .tabItem { Label("Feed", systemImage: "text.bubble.fill") } - .tag(HomeTab.feed) + .tabItem { Label("Today", systemImage: "sun.max.fill") } + .tag(HomeTab.today.rawValue) + .accessibilityIdentifier("field_ios.today.tab") NavigationStack { - MarketView() + CaptureView() } - .tabItem { Label("Market", systemImage: "leaf") } - .tag(HomeTab.market) + .tabItem { Label("Capture", systemImage: "camera.viewfinder") } + .tag(HomeTab.capture.rawValue) + .accessibilityIdentifier("field_ios.capture.tab") + + NavigationStack { + ActivityView() + } + .tabItem { Label("Activity", systemImage: "list.bullet.clipboard.fill") } + .tag(HomeTab.activity.rawValue) + .accessibilityIdentifier("field_ios.activity.tab") NavigationStack { SettingsView() } .tabItem { Label("Settings", systemImage: "gearshape.fill") } - .tag(HomeTab.settings) + .tag(HomeTab.settings.rawValue) + .accessibilityIdentifier("field_ios.settings.tab") } .accessibilityIdentifier("field_ios.home.tabs") } } + +private struct TodayView: View { + @EnvironmentObject private var app: AppState + + var body: some View { + List { + Section { + VStack(alignment: .leading, spacing: 12) { + Text("Today") + .font(.largeTitle.weight(.bold)) + Text(app.accountDisplayName ?? app.username ?? "Field operator") + .font(.title3.weight(.semibold)) + HStack(spacing: 10) { + Label(syncLabel, systemImage: syncImage) + .font(.subheadline.weight(.semibold)) + Spacer() + RelayPill(count: app.relayConnectedCount) + } + } + .padding(.vertical, 4) + } + + 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") + 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") + } + + Section("Diagnostics") { + RelayMetricRow(label: "Connected", systemImage: "dot.radiowaves.left.and.right", value: app.relayConnectedCount) + RelayMetricRow(label: "Connecting", systemImage: "antenna.radiowaves.left.and.right", value: app.relayConnectingCount) + if let last = app.relayLastError { + Text(last) + .foregroundStyle(.red) + .font(.footnote) + } + Text(app.infoJSONString) + .font(.footnote.monospaced()) + .textSelection(.enabled) + .lineLimit(8) + } + } + .listStyle(.insetGrouped) + .inlineNavigationTitle("Today") + .accessibilityIdentifier("field_ios.today") + } + + private var syncLabel: String { + app.relayConnectedCount > 0 ? "Sync online" : "Waiting for relay" + } + + private var syncImage: String { + app.relayConnectedCount > 0 ? "checkmark.icloud.fill" : "icloud.slash.fill" + } +} + +private struct CaptureView: View { + 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") + 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") + } + } + .listStyle(.insetGrouped) + .inlineNavigationTitle("Capture") + .accessibilityIdentifier("field_ios.capture") + } +} + +private struct ActivityView: View { + @EnvironmentObject private var app: AppState + + var body: some View { + List { + Section("Recent Activity") { + ActivityRow(title: "Session ready", detail: app.username ?? "Signed in", systemImage: "person.crop.circle.badge.checkmark") + ActivityRow(title: "Relay posture", detail: "\(app.relayConnectedCount) connected, \(app.relayConnectingCount) connecting", systemImage: "dot.radiowaves.left.and.right") + ActivityRow(title: "Draft queue", detail: "No local drafts", systemImage: "tray") + } + } + .listStyle(.insetGrouped) + .inlineNavigationTitle("Activity") + .accessibilityIdentifier("field_ios.activity") + } +} + +private struct FieldActionRow: View { + let title: String + let subtitle: String + let systemImage: String + + var body: some View { + HStack(spacing: 14) { + Image(systemName: systemImage) + .font(.title3.weight(.semibold)) + .foregroundStyle(.green) + .frame(width: 34, height: 34) + VStack(alignment: .leading, spacing: 4) { + Text(title) + .font(.headline) + Text(subtitle) + .font(.subheadline) + .foregroundStyle(.secondary) + } + } + .padding(.vertical, 4) + .accessibilityElement(children: .combine) + } +} + +private struct ActivityRow: View { + let title: String + let detail: String + let systemImage: String + + var body: some View { + Label { + VStack(alignment: .leading, spacing: 4) { + Text(title) + .font(.headline) + Text(detail) + .font(.subheadline) + .foregroundStyle(.secondary) + } + } icon: { + Image(systemName: systemImage) + .foregroundStyle(.green) + } + .padding(.vertical, 4) + } +} + +private struct RelayPill: View { + let count: UInt32 + + var body: some View { + Text("\(count) connected") + .font(.caption.weight(.semibold)) + .padding(.horizontal, 10) + .padding(.vertical, 6) + .foregroundStyle(count > 0 ? .green : .secondary) + .background(.thinMaterial, in: Capsule()) + } +} + +private struct RelayMetricRow: View { + let label: String + let systemImage: String + let value: UInt32 + + var body: some View { + HStack { + Label(label, systemImage: systemImage) + Spacer() + Text("\(value)") + .foregroundStyle(.secondary) + } + } +}