field_ios

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

commit 0ae444daa24d60a1813a45086dbbdc138944af7c
parent a4be87232936816d49be78f4be69dbe495a294be
Author: triesap <tyson@radroots.org>
Date:   Fri, 19 Jun 2026 16:37:49 -0700

errors: adapt field app to typed runtime failures

- add a Swift adapter for generated Radroots runtime error categories.
- route app error displays through typed runtime messages.
- lock FFI generation to the typed field_lib source revision.
- keep generated bindings and frameworks in the existing ignored output paths.

Diffstat:
MRadroots.xcodeproj/project.pbxproj | 4++++
MRadroots/App/AppState.swift | 16++++++++--------
MRadroots/Runtime/FieldCaptureIntake.swift | 4++--
MRadroots/Runtime/FieldLocationCheckIn.swift | 2+-
ARadroots/Runtime/FieldRuntimeError.swift | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MRadroots/Runtime/Radroots.swift | 2+-
MRadroots/Views/MarketView.swift | 2+-
MRadroots/Views/PostFeedViewModel.swift | 6+++---
MRadroots/Views/RelaysView.swift | 8++++----
MRadroots/Views/SettingsView.swift | 6+++---
MRadroots/Views/SetupView.swift | 6+++---
MRadroots/Views/TradeListingCreateView.swift | 2+-
MRadroots/Views/TradeListingDetailView.swift | 2+-
MRadroots/Views/TradeOrderRequestView.swift | 2+-
MRadrootsFFI/Makefile | 2+-
MRadrootsFFI/source.lock | 2+-
16 files changed, 93 insertions(+), 31 deletions(-)

diff --git a/Radroots.xcodeproj/project.pbxproj b/Radroots.xcodeproj/project.pbxproj @@ -56,6 +56,7 @@ D62E9461833A0AA5E622A1E6 /* ToastModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 227028B4EBDC6703999FB9DA /* ToastModifier.swift */; }; D9BF5BE7E4AB5EACBF342539 /* FieldSecureIdentityStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8246B707FA9D218414EC4038 /* FieldSecureIdentityStore.swift */; }; DCE468F668A3C346E716B04C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CCF0F7B3C57D8D770F178329 /* Assets.xcassets */; }; + DDDA83E35D868FE927D2ED37 /* FieldRuntimeError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32238D3DFB4465DEEDE6E003 /* FieldRuntimeError.swift */; }; E1EDAEE6B182025ACAF754A6 /* RadrootsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15DBA726450712D6DE88E951 /* RadrootsProvider.swift */; }; E3864E34D67BAD0744B93180 /* Bundle+Build.swift in Sources */ = {isa = PBXBuildFile; fileRef = 138AA7BAA021EE13E829390B /* Bundle+Build.swift */; }; E432FD39ECC8F03764EEED81 /* FieldDocumentInterchangeUITestProbe.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF05B944D348DAA4EF28B463 /* FieldDocumentInterchangeUITestProbe.swift */; }; @@ -77,6 +78,7 @@ 26BAE32CD1D46033DDA1A5BB /* TradeListingDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TradeListingDetailView.swift; sourceTree = "<group>"; }; 2818363B157125491FB84A1E /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = "<group>"; }; 2F3541389124A31F5D701A45 /* FieldLocationCheckIn.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldLocationCheckIn.swift; sourceTree = "<group>"; }; + 32238D3DFB4465DEEDE6E003 /* FieldRuntimeError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldRuntimeError.swift; sourceTree = "<group>"; }; 3B4E53FD4C4AADF63855888A /* FieldTelemetry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldTelemetry.swift; sourceTree = "<group>"; }; 3E6187FA7C4786EC662718B2 /* FieldExternalActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldExternalActions.swift; sourceTree = "<group>"; }; 41A4289F43625DD65E6C4B25 /* CopyRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyRow.swift; sourceTree = "<group>"; }; @@ -218,6 +220,7 @@ CA942715DB13FFD494FD35A0 /* FieldIdentityPublicMetadataStore.swift */, 9EB0A2CFCBBFA9D204C6992B /* FieldLocalState.swift */, 2F3541389124A31F5D701A45 /* FieldLocationCheckIn.swift */, + 32238D3DFB4465DEEDE6E003 /* FieldRuntimeError.swift */, E883FDB7004A210C9D7BE27A /* FieldRuntimeService.swift */, 8246B707FA9D218414EC4038 /* FieldSecureIdentityStore.swift */, 3B4E53FD4C4AADF63855888A /* FieldTelemetry.swift */, @@ -459,6 +462,7 @@ D4CFDE54747B6D6957977025 /* FieldIdentityPublicMetadataStore.swift in Sources */, 6E15D30653861F26AC45B501 /* FieldLocalState.swift in Sources */, 1C6EA551530A46CA77BD9E1C /* FieldLocationCheckIn.swift in Sources */, + DDDA83E35D868FE927D2ED37 /* FieldRuntimeError.swift in Sources */, D25C1E1DC99F5CF8E99AE970 /* FieldRuntimeService.swift in Sources */, D9BF5BE7E4AB5EACBF342539 /* FieldSecureIdentityStore.swift in Sources */, 9346DA48630668A65D37E14A /* FieldTelemetry.swift in Sources */, diff --git a/Radroots/App/AppState.swift b/Radroots/App/AppState.swift @@ -185,7 +185,7 @@ public final class AppState: ObservableObject { telemetryProbeTask = nil await FieldBackgroundURLSessionEvents.shared.completePendingAfterStartupFailure() backgroundExecution = nil - let message = error.localizedDescription + let message = error.fieldRuntimeMessage bootstrapPhase = .failed(message) telemetry.appStartupFailed(error) startTelemetryProbeRefreshForUITest() @@ -541,7 +541,7 @@ public final class AppState: ObservableObject { } catch { captureIntakeState.support = .unavailable captureIntakeState.operation = .idle - captureIntakeState.lastError = error.localizedDescription + captureIntakeState.lastError = error.fieldRuntimeMessage captureIntakeState.recoveryAction = nil telemetry.captureSupportRefreshed( support: captureIntakeState.support, @@ -576,7 +576,7 @@ public final class AppState: ObservableObject { ) } catch { captureIntakeState.operation = .idle - captureIntakeState.lastError = error.localizedDescription + captureIntakeState.lastError = error.fieldRuntimeMessage captureIntakeState.recoveryAction = captureRecoveryAction(for: error) telemetry.captureOperation( operation: operation, @@ -674,7 +674,7 @@ public final class AppState: ObservableObject { let snapshot = try await service.nostrIdentitySnapshot() apply(identity: snapshot) } catch { - relayLastError = error.localizedDescription + relayLastError = error.fieldRuntimeMessage } await refreshRelayStatus(using: service) await backgroundExecution?.updateRuntimeState( @@ -768,7 +768,7 @@ public final class AppState: ObservableObject { userPresenceStatus = record.statusText telemetry.userPresence(action: action, outcome: "success") } catch { - userPresenceStatus = error.localizedDescription + userPresenceStatus = error.fieldRuntimeMessage telemetry.userPresence(action: action, outcome: FieldTelemetry.userPresenceOutcome(for: error)) throw error } @@ -801,7 +801,7 @@ public final class AppState: ObservableObject { do { try await lockRuntimeIdentityState(using: service) } catch { - relayLastError = error.localizedDescription + relayLastError = error.fieldRuntimeMessage } hasKey = storedIdentityAvailable await refreshRelayStatus(using: service) @@ -912,7 +912,7 @@ public final class AppState: ObservableObject { externalActionStatus = record.statusText telemetry.externalAction(operation: "open", kind: record.kind, outcome: "success") } catch { - externalActionStatus = error.localizedDescription + externalActionStatus = error.fieldRuntimeMessage telemetry.externalAction( operation: "open", kind: nil, @@ -932,7 +932,7 @@ public final class AppState: ObservableObject { do { try await self?.connect(using: service) } catch { - self?.relayLastError = error.localizedDescription + self?.relayLastError = error.fieldRuntimeMessage self?.relayLight = .red } while !Task.isCancelled { diff --git a/Radroots/Runtime/FieldCaptureIntake.swift b/Radroots/Runtime/FieldCaptureIntake.swift @@ -382,7 +382,7 @@ final class FieldCaptureIntake: @unchecked Sendable { success: success ) } catch { - return .failure(.permanentFailure(error.localizedDescription)) + return .failure(.permanentFailure(error.fieldRuntimeMessage)) } } @@ -395,7 +395,7 @@ final class FieldCaptureIntake: @unchecked Sendable { success: success ) } catch { - return .failure(.permanentFailure(error.localizedDescription)) + return .failure(.permanentFailure(error.fieldRuntimeMessage)) } } diff --git a/Radroots/Runtime/FieldLocationCheckIn.swift b/Radroots/Runtime/FieldLocationCheckIn.swift @@ -95,7 +95,7 @@ public struct FieldLocationCheckIn: Sendable { } catch RadrootsLocationServicesError.cancelled(let message) { return .failed(availability, message) } catch { - return .failed(availability, error.localizedDescription) + return .failed(availability, error.fieldRuntimeMessage) } } diff --git a/Radroots/Runtime/FieldRuntimeError.swift b/Radroots/Runtime/FieldRuntimeError.swift @@ -0,0 +1,58 @@ +import Foundation + +enum FieldRuntimeErrorCategory: String, Sendable { + case initialization + case identity + case secureStore + case relay + case runtime + case unsupported + case internalFailure +} + +extension RadrootsAppError { + var fieldCategory: FieldRuntimeErrorCategory { + switch self { + case .Initialization(_): + .initialization + case .Identity(_): + .identity + case .SecureStore(_): + .secureStore + case .Relay(_): + .relay + case .Runtime(_): + .runtime + case .Unsupported(_): + .unsupported + case .Internal(_): + .internalFailure + } + } + + var fieldMessage: String { + switch self { + case .Initialization(let message), + .Identity(let message), + .SecureStore(let message), + .Relay(let message), + .Runtime(let message), + .Unsupported(let message), + .Internal(let message): + message + } + } +} + +extension Error { + var fieldRuntimeErrorCategory: FieldRuntimeErrorCategory? { + (self as? RadrootsAppError)?.fieldCategory + } + + var fieldRuntimeMessage: String { + if let fieldError = self as? RadrootsAppError { + return fieldError.fieldMessage + } + return localizedDescription + } +} diff --git a/Radroots/Runtime/Radroots.swift b/Radroots/Runtime/Radroots.swift @@ -18,7 +18,7 @@ public final class Radroots: ObservableObject { do { try settings.apply(bundleIdentifier: bundleId) } catch { - throw FieldRuntimeLoggingError.initializationFailed(error.localizedDescription) + throw FieldRuntimeLoggingError.initializationFailed(error.fieldRuntimeMessage) } telemetry.runtimeLoggingInitialized(settings: settings) diff --git a/Radroots/Views/MarketView.swift b/Radroots/Views/MarketView.swift @@ -23,7 +23,7 @@ final class TradeListingsViewModel: ObservableObject { listings = items isLoading = false } catch { - errorMessage = String(describing: error) + errorMessage = error.fieldRuntimeMessage isLoading = false } } diff --git a/Radroots/Views/PostFeedViewModel.swift b/Radroots/Views/PostFeedViewModel.swift @@ -33,7 +33,7 @@ final class PostFeedViewModel: ObservableObject { posts = fetched.sorted { $0.publishedAt > $1.publishedAt } isLoading = false } catch { - errorMessage = String(describing: error) + errorMessage = error.fieldRuntimeMessage isLoading = false } } @@ -82,7 +82,7 @@ final class PostFeedViewModel: ObservableObject { setResult("Reply Posted", "Event \(id.rawValue)") } catch { sendingReplyFor.remove(parentId) - setResult("Failed to Post Reply", String(describing: error)) + setResult("Failed to Post Reply", error.fieldRuntimeMessage) } } } @@ -97,7 +97,7 @@ final class PostFeedViewModel: ObservableObject { do { try await app.runtimeService?.nostrStartPostStream(sinceUnix: since) } catch { - errorMessage = String(describing: error) + errorMessage = error.fieldRuntimeMessage } while !Task.isCancelled { diff --git a/Radroots/Views/RelaysView.swift b/Radroots/Views/RelaysView.swift @@ -120,7 +120,7 @@ struct RelaysView: View { activeExport = export preparedExport = export } catch { - documentError = error.localizedDescription + documentError = error.fieldRuntimeMessage } } @@ -137,7 +137,7 @@ struct RelaysView: View { destinationScope: .temporary ) } catch { - documentError = error.localizedDescription + documentError = error.fieldRuntimeMessage } } @@ -151,7 +151,7 @@ struct RelaysView: View { documentMessage = "Exported \(exportResult.exportedFilename)" documentError = nil case .failure(let error): - documentError = error.localizedDescription + documentError = error.fieldRuntimeMessage } } @@ -172,7 +172,7 @@ struct RelaysView: View { documentMessage = "Imported and applied \(importedRelays.count) relay config entries" documentError = nil } catch { - documentError = error.localizedDescription + documentError = error.fieldRuntimeMessage } } } diff --git a/Radroots/Views/SettingsView.swift b/Radroots/Views/SettingsView.swift @@ -141,7 +141,7 @@ struct SettingsView: View { do { try await app.resetLocalIdentity() } catch { - resetError = error.localizedDescription + resetError = error.fieldRuntimeMessage } } } @@ -223,7 +223,7 @@ private struct RuntimeDiagnosticsView: View { activeExport = export preparedExport = export } catch { - exportError = error.localizedDescription + exportError = error.fieldRuntimeMessage } } @@ -237,7 +237,7 @@ private struct RuntimeDiagnosticsView: View { exportMessage = "Exported \(exportResult.exportedFilename)" exportError = nil case .failure(let error): - exportError = error.localizedDescription + exportError = error.fieldRuntimeMessage } } } diff --git a/Radroots/Views/SetupView.swift b/Radroots/Views/SetupView.swift @@ -133,7 +133,7 @@ struct SetupView: View { try await app.continueWithLocalIdentity() onSuccess?() } catch { - errorMessage = error.localizedDescription + errorMessage = error.fieldRuntimeMessage } isWorking = false } @@ -147,7 +147,7 @@ struct SetupView: View { try await app.createLocalIdentity() onSuccess?() } catch { - errorMessage = error.localizedDescription + errorMessage = error.fieldRuntimeMessage } isWorking = false } @@ -163,7 +163,7 @@ struct SetupView: View { try await app.importNostrSecret(submittedSecret) onSuccess?() } catch { - errorMessage = error.localizedDescription + errorMessage = error.fieldRuntimeMessage } isWorking = false } diff --git a/Radroots/Views/TradeListingCreateView.swift b/Radroots/Views/TradeListingCreateView.swift @@ -151,7 +151,7 @@ struct TradeListingCreateView: View { dismiss() } catch { isPosting = false - errorMessage = String(describing: error) + errorMessage = error.fieldRuntimeMessage } } } diff --git a/Radroots/Views/TradeListingDetailView.swift b/Radroots/Views/TradeListingDetailView.swift @@ -27,7 +27,7 @@ final class TradeListingDetailViewModel: ObservableObject { messages = items isLoading = false } catch { - errorMessage = String(describing: error) + errorMessage = error.fieldRuntimeMessage isLoading = false } } diff --git a/Radroots/Views/TradeOrderRequestView.swift b/Radroots/Views/TradeOrderRequestView.swift @@ -129,7 +129,7 @@ struct TradeOrderRequestView: View { dismiss() } catch { isSending = false - errorMessage = String(describing: error) + errorMessage = error.fieldRuntimeMessage } } } diff --git a/RadrootsFFI/Makefile b/RadrootsFFI/Makefile @@ -6,7 +6,7 @@ SHELL := /bin/bash SOURCE_MODE ?= git RADROOTS_FIELD_LIB_GIT_URL ?= git@github.com:radrootslabs/field_lib.git -RADROOTS_FIELD_LIB_GIT_REV ?= daedde8b8022190954ff4a68e34ed93b3423be34 +RADROOTS_FIELD_LIB_GIT_REV ?= 20af1b91483cf420170bfb03d12d86da10f34363 RADROOTS_FIELD_FFI_CRATE_VERSION ?= 0.1.0-alpha.1 FFI_FEATURES ?= radroots_field_core/rt,radroots_field_core/nostr-client LOCAL_FFI_MANIFEST ?= diff --git a/RadrootsFFI/source.lock b/RadrootsFFI/source.lock @@ -1,5 +1,5 @@ SOURCE_MODE=git RADROOTS_FIELD_LIB_GIT_URL=git@github.com:radrootslabs/field_lib.git -RADROOTS_FIELD_LIB_GIT_REV=daedde8b8022190954ff4a68e34ed93b3423be34 +RADROOTS_FIELD_LIB_GIT_REV=20af1b91483cf420170bfb03d12d86da10f34363 RADROOTS_FIELD_FFI_CRATE_VERSION=0.1.0-alpha.1 FFI_FEATURES=radroots_field_core/rt,radroots_field_core/nostr-client