field_ios

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

commit 29565094868a88a103526a7275bb816bb63c6f62
parent 0ae444daa24d60a1813a45086dbbdc138944af7c
Author: triesap <tyson@radroots.org>
Date:   Fri, 19 Jun 2026 16:46:05 -0700

runtime: handle typed mobile read outcomes

- call throwing runtime reads from the Swift service boundary.
- surface profile no-data separately from typed profile read failures.
- keep post stream polling resilient to typed stream failures.
- lock iOS FFI generation to the runtime-read field_lib revision.

Diffstat:
MRadroots/Runtime/FieldRuntimeService.swift | 8++++----
MRadroots/Views/PostCreateView.swift | 2+-
MRadroots/Views/PostFeedViewModel.swift | 22++++++++++++++--------
MRadroots/Views/ProfileView.swift | 31++++++++++++++++++++++---------
MRadrootsFFI/Makefile | 2+-
MRadrootsFFI/source.lock | 2+-
6 files changed, 43 insertions(+), 24 deletions(-)

diff --git a/Radroots/Runtime/FieldRuntimeService.swift b/Radroots/Runtime/FieldRuntimeService.swift @@ -82,8 +82,8 @@ public final class FieldRuntimeService: @unchecked Sendable { try await run { try $0.nostrIdentityResetHostCustodyRuntime() } } - public func nostrProfileForSelf() async -> NostrProfileEventMetadata? { - await runValue { $0.nostrProfileForSelf() } + public func nostrProfileForSelf() async throws -> NostrProfileEventMetadata? { + try await run { try $0.nostrProfileForSelf() } } public func nostrFetchTextNotes( @@ -93,7 +93,7 @@ public final class FieldRuntimeService: @unchecked Sendable { try await run { try $0.nostrFetchTextNotes(limit: limit, sinceUnix: sinceUnix) } } - public func nostrNextPostStreamEvent() async -> NostrPostEventMetadata? { - await runValue { $0.nostrNextPostEvent() } + public func nostrNextPostStreamEvent() async throws -> NostrPostEventMetadata? { + try await run { try $0.nostrNextPostEvent() } } } diff --git a/Radroots/Views/PostCreateView.swift b/Radroots/Views/PostCreateView.swift @@ -93,7 +93,7 @@ struct PostCreateView: View { } } catch { await MainActor.run { - resultMessage = "Failed to post: \(error)" + resultMessage = "Failed to post: \(error.fieldRuntimeMessage)" showResult = true isPosting = false } diff --git a/Radroots/Views/PostFeedViewModel.swift b/Radroots/Views/PostFeedViewModel.swift @@ -92,10 +92,11 @@ final class PostFeedViewModel: ObservableObject { guard liveTask == nil else { return } liveTask = Task { @MainActor [weak self] in guard let self else { return } + guard let service = app.runtimeService else { return } var knownIds = Set(posts.map(\.id)) let since = posts.map(\.publishedAt).max() do { - try await app.runtimeService?.nostrStartPostStream(sinceUnix: since) + try await service.nostrStartPostStream(sinceUnix: since) } catch { errorMessage = error.fieldRuntimeMessage } @@ -110,15 +111,20 @@ final class PostFeedViewModel: ObservableObject { knownIds = Set(posts.map(\.id)) } - if let event = await app.runtimeService?.nostrNextPostStreamEvent() { - if knownIds.insert(event.id).inserted { - posts.insert(event, at: 0) - posts.sort { $0.publishedAt > $1.publishedAt } - if posts.count > 200 { - posts = Array(posts.prefix(200)) + do { + if let event = try await service.nostrNextPostStreamEvent() { + if knownIds.insert(event.id).inserted { + posts.insert(event, at: 0) + posts.sort { $0.publishedAt > $1.publishedAt } + if posts.count > 200 { + posts = Array(posts.prefix(200)) + } } + } else { + try? await Task.sleep(for: .milliseconds(300)) } - } else { + } catch { + errorMessage = error.fieldRuntimeMessage try? await Task.sleep(for: .milliseconds(300)) } } diff --git a/Radroots/Views/ProfileView.swift b/Radroots/Views/ProfileView.swift @@ -11,6 +11,7 @@ public struct ProfileView: View { @State private var original: OriginalProfile = .empty @State private var isLoading: Bool = false @State private var isPosting: Bool = false + @State private var profileReadMessage: String? @State private var postMessage: String? @State private var showMessage: Bool = false @FocusState private var focusedField: Field? @@ -68,6 +69,9 @@ public struct ProfileView: View { .animation(.easeInOut(duration: 0.15), value: hasChanges) } footer: { VStack(alignment: .leading, spacing: 6) { + if let profileReadMessage { + Text(profileReadMessage) + } if !isConnected { Text("No relays connected. Connect to at least one relay to post.") } @@ -111,14 +115,23 @@ public struct ProfileView: View { guard let service = app.runtimeService else { return } isLoading = true Task { - let meta = await service.nostrProfileForSelf() - await MainActor.run { - self.original = OriginalProfile.from(meta) - self.name = original.name - self.displayName = original.displayName - self.nip05 = original.nip05 - self.about = original.about - self.isLoading = false + do { + let meta = try await service.nostrProfileForSelf() + let loaded = OriginalProfile.from(meta) + await MainActor.run { + self.original = loaded + self.name = loaded.name + self.displayName = loaded.displayName + self.nip05 = loaded.nip05 + self.about = loaded.about + self.profileReadMessage = meta == nil ? "No profile event found yet." : nil + self.isLoading = false + } + } catch { + await MainActor.run { + self.profileReadMessage = error.fieldRuntimeMessage + self.isLoading = false + } } } } @@ -146,7 +159,7 @@ public struct ProfileView: View { } catch { await MainActor.run { self.isPosting = false - self.postMessage = "Failed to post profile: \(error)" + self.postMessage = "Failed to post profile: \(error.fieldRuntimeMessage)" self.showMessage = true } } 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 ?= 20af1b91483cf420170bfb03d12d86da10f34363 +RADROOTS_FIELD_LIB_GIT_REV ?= 826c68898139e5119c6388fece76df6b853f6458 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=20af1b91483cf420170bfb03d12d86da10f34363 +RADROOTS_FIELD_LIB_GIT_REV=826c68898139e5119c6388fece76df6b853f6458 RADROOTS_FIELD_FFI_CRATE_VERSION=0.1.0-alpha.1 FFI_FEATURES=radroots_field_core/rt,radroots_field_core/nostr-client