field_ios

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

AppEntry.swift (4988B)


      1 import SwiftUI
      2 
      3 public struct AppEntry<Main: View>: View {
      4     @EnvironmentObject private var appState: AppState
      5     private let main: () -> Main
      6 
      7     public init(@ViewBuilder main: @escaping () -> Main) {
      8         self.main = main
      9     }
     10 
     11     public var body: some View {
     12         Group {
     13             switch appState.bootstrapPhase {
     14             case .idle, .starting:
     15                 SplashView()
     16             case .failed(let message):
     17                 StartupFailureView(message: message) {
     18                     appState.retryStartup()
     19                 }
     20             case .ready:
     21                 if appState.canShowAppContent {
     22                     main()
     23                 } else {
     24                     NavigationStack {
     25                         SetupView()
     26                     }
     27                 }
     28             }
     29         }
     30         .accessibilityIdentifier("field_ios.app_entry")
     31         #if DEBUG
     32         .overlay(alignment: .topLeading) {
     33             if let probeValue = appState.fileAccessProbeValue {
     34                 Color.clear
     35                     .frame(width: 1, height: 1)
     36                     .accessibilityElement()
     37                     .accessibilityIdentifier("field_ios.file_access.probe")
     38                     .accessibilityValue(probeValue)
     39             }
     40             if let probeValue = appState.documentInterchangeProbeValue {
     41                 Color.clear
     42                     .frame(width: 1, height: 1)
     43                     .accessibilityElement()
     44                     .accessibilityIdentifier("field_ios.document_interchange.probe")
     45                     .accessibilityValue(probeValue)
     46             }
     47             if let probeValue = appState.identityPolicyProbeValue {
     48                 Color.clear
     49                     .frame(width: 1, height: 1)
     50                     .accessibilityElement()
     51                     .accessibilityIdentifier("field_ios.identity_policy.probe")
     52                     .accessibilityValue(probeValue)
     53             }
     54             if let probeValue = appState.identityImportFailureProbeValue {
     55                 Color.clear
     56                     .frame(width: 1, height: 1)
     57                     .accessibilityElement()
     58                     .accessibilityIdentifier("field_ios.identity_import_failure.probe")
     59                     .accessibilityValue(probeValue)
     60             }
     61             if let probeValue = appState.telemetryProbeValue {
     62                 Color.clear
     63                     .frame(width: 1, height: 1)
     64                     .accessibilityElement()
     65                     .accessibilityIdentifier("field_ios.telemetry.probe")
     66                     .accessibilityValue(probeValue)
     67             }
     68             if let probeValue = appState.backgroundExecutionProbeValue {
     69                 Color.clear
     70                     .frame(width: 1, height: 1)
     71                     .accessibilityElement()
     72                     .accessibilityIdentifier("field_ios.background_execution.probe")
     73                     .accessibilityValue(probeValue)
     74             }
     75         }
     76         #endif
     77     }
     78 }
     79 
     80 private struct StartupFailureView: View {
     81     let message: String
     82     let onRetry: () -> Void
     83 
     84     var body: some View {
     85         VStack(spacing: 18) {
     86             Spacer()
     87             Image(systemName: "exclamationmark.triangle.fill")
     88                 .font(.system(size: 56, weight: .semibold))
     89                 .foregroundStyle(.red)
     90             Text("Startup failed")
     91                 .font(.title2.weight(.semibold))
     92             Text(message)
     93                 .font(.footnote)
     94                 .foregroundStyle(.secondary)
     95                 .multilineTextAlignment(.center)
     96                 .textSelection(.enabled)
     97             Spacer()
     98             Button {
     99                 onRetry()
    100             } label: {
    101                 Label("Retry", systemImage: "arrow.clockwise")
    102                     .frame(maxWidth: .infinity)
    103             }
    104             .buttonStyle(.borderedProminent)
    105             .controlSize(.large)
    106             .accessibilityIdentifier("field_ios.bootstrap.retry")
    107         }
    108         .padding()
    109         .accessibilityIdentifier("field_ios.bootstrap.failed")
    110     }
    111 }
    112 
    113 private struct SplashView: View {
    114     private let splashGlyphSize: CGFloat = 160
    115 
    116     var body: some View {
    117         ZStack {
    118             Color("RadrootsSplashBackground")
    119                 .ignoresSafeArea()
    120 
    121             Color.clear
    122                 .accessibilityElement()
    123                 .accessibilityLabel("Startup")
    124                 .accessibilityIdentifier("field_ios.bootstrap")
    125 
    126             GeometryReader { proxy in
    127                 Image("RadrootsSplashLogomark")
    128                     .resizable()
    129                     .scaledToFit()
    130                     .frame(width: splashGlyphSize, height: splashGlyphSize)
    131                     .position(x: proxy.size.width / 2, y: proxy.size.height / 2)
    132                     .accessibilityLabel("Radroots")
    133                     .accessibilityIdentifier("field_ios.splash.logo")
    134             }
    135             .ignoresSafeArea()
    136         }
    137         .accessibilityElement(children: .contain)
    138     }
    139 }