field_ios

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

commit e1ce6d1f43d910aef6960c94af0052d618b958a6
parent bd3bf69ee2013b80afb96278658cc4db82a8157b
Author: triesap <tyson@radroots.org>
Date:   Sat, 13 Jun 2026 13:51:05 -0700

app: add launch splash screen

- point the native launch screen at generated splash assets
- render bootstrap startup with the branded logomark
- add a UI-test-only splash hold for deterministic checks
- track the splash asset catalog resources

Diffstat:
MRadroots/App/AppEntry.swift | 24+++++++++++++++++++++---
MRadroots/App/AppState.swift | 29++++++++++++++++++++++++++---
MRadroots/Info.plist | 7++++++-
ARadroots/Resources/Assets.xcassets/RadrootsSplashBackground.colorset/Contents.json | 20++++++++++++++++++++
ARadroots/Resources/Assets.xcassets/RadrootsSplashLogomark.imageset/Contents.json | 23+++++++++++++++++++++++
ARadroots/Resources/Assets.xcassets/RadrootsSplashLogomark.imageset/RadrootsSplashLogomark.png | 0
ARadroots/Resources/Assets.xcassets/RadrootsSplashLogomark.imageset/RadrootsSplashLogomark@2x.png | 0
ARadroots/Resources/Assets.xcassets/RadrootsSplashLogomark.imageset/RadrootsSplashLogomark@3x.png | 0
8 files changed, 96 insertions(+), 7 deletions(-)

diff --git a/Radroots/App/AppEntry.swift b/Radroots/App/AppEntry.swift @@ -65,11 +65,29 @@ private struct StartupFailureView: View { } private struct SplashView: View { + private let splashGlyphSize: CGFloat = 160 + var body: some View { ZStack { - Color(.systemBackground).ignoresSafeArea() - ProgressView().controlSize(.large) + Color("RadrootsSplashBackground") + .ignoresSafeArea() + + Color.clear + .accessibilityElement() + .accessibilityLabel("Startup") + .accessibilityIdentifier("field_ios.bootstrap") + + GeometryReader { proxy in + Image("RadrootsSplashLogomark") + .resizable() + .scaledToFit() + .frame(width: splashGlyphSize, height: splashGlyphSize) + .position(x: proxy.size.width / 2, y: proxy.size.height / 2) + .accessibilityLabel("Radroots") + .accessibilityIdentifier("field_ios.splash.logo") + } + .ignoresSafeArea() } - .accessibilityIdentifier("field_ios.bootstrap") + .accessibilityElement(children: .contain) } } diff --git a/Radroots/App/AppState.swift b/Radroots/App/AppState.swift @@ -84,6 +84,7 @@ public final class AppState: ObservableObject { guard bootstrapPhase == .idle || isFailed else { return } bootstrapPhase = .starting do { + try await holdBootstrapSplashForUITestIfRequested() if startupFailureWasRequested { throw FieldAppRuntimeError.forcedStartupFailure } @@ -205,13 +206,35 @@ public final class AppState: ObservableObject { return false } - private var startupFailureWasRequested: Bool { + private var uiTestWasRequested: Bool { let arguments = ProcessInfo.processInfo.arguments let environment = ProcessInfo.processInfo.environment - guard environment["RADROOTS_FIELD_IOS_UI_TEST"] == "true" || - arguments.contains("--radroots-field-ios-ui-test") else { + return environment["RADROOTS_FIELD_IOS_UI_TEST"] == "true" || + arguments.contains("--radroots-field-ios-ui-test") + } + + private var uiTestBootstrapSplashHoldNanoseconds: UInt64? { + guard uiTestWasRequested else { return nil } + guard let raw = ProcessInfo.processInfo.environment["RADROOTS_FIELD_IOS_UI_TEST_BOOTSTRAP_SPLASH_HOLD_SECONDS"], + let seconds = Double(raw), + seconds.isFinite, + seconds > 0 else { + return nil + } + return UInt64(seconds * 1_000_000_000) + } + + private func holdBootstrapSplashForUITestIfRequested() async throws { + guard let nanoseconds = uiTestBootstrapSplashHoldNanoseconds else { return } + try await Task.sleep(nanoseconds: nanoseconds) + } + + private var startupFailureWasRequested: Bool { + guard uiTestWasRequested else { return false } + let arguments = ProcessInfo.processInfo.arguments + let environment = ProcessInfo.processInfo.environment if BuildConfig.string(.runtimeMode) == "ui-test-startup-failure" { return true } diff --git a/Radroots/Info.plist b/Radroots/Info.plist @@ -41,7 +41,12 @@ </dict> <key>UILaunchScreen</key> - <dict/> + <dict> + <key>UIColorName</key> + <string>RadrootsSplashBackground</string> + <key>UIImageName</key> + <string>RadrootsSplashLogomark</string> + </dict> <key>UISupportedInterfaceOrientations</key> <array> diff --git a/Radroots/Resources/Assets.xcassets/RadrootsSplashBackground.colorset/Contents.json b/Radroots/Resources/Assets.xcassets/RadrootsSplashBackground.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors": [ + { + "color": { + "color-space": "srgb", + "components": { + "red": "0.333333", + "green": "0.600000", + "blue": "0.247059", + "alpha": "1.000000" + } + }, + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/Radroots/Resources/Assets.xcassets/RadrootsSplashLogomark.imageset/Contents.json b/Radroots/Resources/Assets.xcassets/RadrootsSplashLogomark.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images": [ + { + "filename": "RadrootsSplashLogomark.png", + "idiom": "universal", + "scale": "1x" + }, + { + "filename": "RadrootsSplashLogomark@2x.png", + "idiom": "universal", + "scale": "2x" + }, + { + "filename": "RadrootsSplashLogomark@3x.png", + "idiom": "universal", + "scale": "3x" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/Radroots/Resources/Assets.xcassets/RadrootsSplashLogomark.imageset/RadrootsSplashLogomark.png b/Radroots/Resources/Assets.xcassets/RadrootsSplashLogomark.imageset/RadrootsSplashLogomark.png Binary files differ. diff --git a/Radroots/Resources/Assets.xcassets/RadrootsSplashLogomark.imageset/RadrootsSplashLogomark@2x.png b/Radroots/Resources/Assets.xcassets/RadrootsSplashLogomark.imageset/RadrootsSplashLogomark@2x.png Binary files differ. diff --git a/Radroots/Resources/Assets.xcassets/RadrootsSplashLogomark.imageset/RadrootsSplashLogomark@3x.png b/Radroots/Resources/Assets.xcassets/RadrootsSplashLogomark.imageset/RadrootsSplashLogomark@3x.png Binary files differ.