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 }