field_ios

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

commit 9af86307f7de9b4914660fab768be1b9a048a401
parent 7754b85b01a45adc0160e6b084713cc9c53b5385
Author: triesap <tyson@radroots.org>
Date:   Sun, 14 Jun 2026 17:18:13 -0700

field-ios: stabilize app shell startup actions

- open local identity setup without blocking on relay connect

- keep relay status polling in the background

- preserve location check-in result state across view refreshes

- make location check-in use the full-row capture action affordance

Diffstat:
MRadroots/App/AppState.swift | 36+++++++++++++++++++++++++-----------
MRadroots/Views/HomeView.swift | 22++++++++++++++++++----
2 files changed, 43 insertions(+), 15 deletions(-)

diff --git a/Radroots/App/AppState.swift b/Radroots/App/AppState.swift @@ -125,8 +125,7 @@ public final class AppState: ObservableObject { self.captureIntake = captureIntake await refreshRuntimeState(using: service) if runtimeIdentityReady && !isLocked { - try await connect(using: service) - startPollingStatus() + startConnectingAndPollingStatus(using: service) } await refreshNostrProfileExternalActionCapability() try refreshFileAccessProbe( @@ -164,20 +163,18 @@ public final class AppState: ObservableObject { let service = try requireRuntimeService() try await restoreStoredIdentity(using: service) setLocked(false) - try await connect(using: service) await refreshRuntimeState(using: service) await refreshNostrProfileExternalActionCapability() - startPollingStatus() + startConnectingAndPollingStatus(using: service) } public func createLocalIdentity() async throws { let service = try requireRuntimeService() try await createHostCustodyIdentity(using: service) setLocked(false) - try await connect(using: service) await refreshRuntimeState(using: service) await refreshNostrProfileExternalActionCapability() - startPollingStatus() + startConnectingAndPollingStatus(using: service) } public func importNostrSecret(_ secretKey: String) async throws { @@ -191,10 +188,9 @@ public final class AppState: ObservableObject { ) try persistIdentity(record) setLocked(false) - try await connect(using: service) await refreshRuntimeState(using: service) await refreshNostrProfileExternalActionCapability() - startPollingStatus() + startConnectingAndPollingStatus(using: service) } public func signOut() { @@ -240,7 +236,19 @@ public final class AppState: ObservableObject { } public func refreshLocationCheckInStatus() async { - locationCheckInState = await locationCheckIn.status() + switch locationCheckInState { + case .idle: + break + case .checking, .checkedIn, .failed: + return + } + let refreshedState = await locationCheckIn.status() + switch locationCheckInState { + case .idle: + locationCheckInState = refreshedState + case .checking, .checkedIn, .failed: + return + } } public func performLocationCheckIn() async { @@ -650,11 +658,17 @@ public final class AppState: ObservableObject { UserDefaults.standard.set(value, forKey: lockKey) } - private func startPollingStatus() { + private func startConnectingAndPollingStatus(using service: FieldRuntimeService) { statusTask?.cancel() statusTask = Task { [weak self] in + do { + try await self?.connect(using: service) + } catch { + self?.relayLastError = error.localizedDescription + self?.relayLight = .red + } while !Task.isCancelled { - await self?.refreshRuntimeState() + await self?.refreshRuntimeState(using: service) try? await Task.sleep(nanoseconds: 1_000_000_000) } } diff --git a/Radroots/Views/HomeView.swift b/Radroots/Views/HomeView.swift @@ -414,11 +414,25 @@ private struct LocationCheckInRow: View { await app.performLocationCheckIn() } } label: { - Label(actionTitle, systemImage: "location.fill") - .font(.subheadline.weight(.semibold)) - .frame(maxWidth: .infinity) + HStack(spacing: 14) { + Image(systemName: "location.fill") + .font(.title3.weight(.semibold)) + .frame(width: 34, height: 34) + .accessibilityHidden(true) + Text(actionTitle) + .font(.headline) + Spacer() + if isChecking { + ProgressView() + } else { + Image(systemName: "chevron.right") + .font(.footnote.weight(.semibold)) + .foregroundStyle(.tertiary) + .accessibilityHidden(true) + } + } + .padding(.vertical, 4) } - .buttonStyle(.borderedProminent) .disabled(isChecking) .accessibilityIdentifier("\(accessibilityIDPrefix).action") }