Swift 100%
1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2// KAApp.swift — point d'entrée : onboarding animé au premier lancement, puis3// la super-app v3 : 6 onglets (dont Ka Trajet, le GPS maison) servis par le4// dock flottant signature (encre + lime) avec KA Agent intégré.5import SwiftUI67@main8struct KAApp: App {9 @AppStorage("ka.onboarded") private var onboarded = false10 @AppStorage("ka.appearance") private var appearance = "clair"11 @StateObject private var favorites = FavoritesStore.shared1213 init() {14 // cache réseau généreux : images et JSON fluides, consultation hors ligne15 URLCache.shared = URLCache(memoryCapacity: 64 * 1024 * 1024,16 diskCapacity: 512 * 1024 * 1024)17 }1819 var body: some Scene {20 WindowGroup {21 Group {22 if onboarded {23 RootView()24 } else {25 OnboardingView()26 }27 }28 .environmentObject(favorites)29 .environmentObject(RecentsStore.shared)30 .preferredColorScheme(appearance == "clair" ? .light : appearance == "sombre" ? .dark : nil)31 }32 }33}3435// MARK: - Racine : 6 onglets servis par le dock flottant signature3637struct RootView: View {38 @State private var showAgent = false39 // onglet initial pilotable en debug — JAMAIS pendant les tests (les threads40 // MapLibre de la carte feraient une course avec le exit() de XCTest)41 @State private var tab: Int42 // onglets déjà visités : montés une fois, gardés vivants (état préservé,43 // cartes MapLibre non réinitialisées) — les autres restent paresseux44 @State private var loaded: Set<Int>45 @StateObject private var recents = RecentsStore.shared46 @StateObject private var trajet = TrajetModel()4748 init() {49 let initial = ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil50 ? 0 : UserDefaults.standard.integer(forKey: "ka.debug.tab")51 _tab = State(initialValue: initial)52 _loaded = State(initialValue: [initial])53 }5455 /// Le dock s'efface dans les modes immersifs de Trajet56 /// (conduite et Découvrir plein écran)57 private var dockVisible: Bool { !trajet.navigating && !trajet.discovering }58 @Environment(\.colorScheme) private var scheme5960 var body: some View {61 // le dock occupe SON espace, sous le contenu : rien ne passe jamais62 // dessous, sur aucune page — c'est une vraie barre, pas un flotteur63 VStack(spacing: 0) {64 ZStack {65 pane(0) { HomeView() }66 pane(1) { SearchView() }67 pane(2) { UniversesView() }68 pane(3) { MapTab() }69 pane(4) { TrajetView().environmentObject(trajet) }70 pane(5) { MoreTab() }71 }72 if dockVisible {73 KADock(tab: $tab, alertCount: recents.alertCount) { showAgent = true }74 .padding(.top, 8)75 .padding(.bottom, 2)76 .transition(.move(edge: .bottom).combined(with: .opacity))77 }78 }79 .background(KATheme.paper(scheme).ignoresSafeArea())80 .tint(KATheme.green)81 .animation(.spring(response: 0.4, dampingFraction: 0.8), value: trajet.navigating)82 .animation(.spring(response: 0.4, dampingFraction: 0.8), value: trajet.discovering)83 .onChange(of: tab) { _, t in loaded.insert(t) }84 // navigation demandée par une vue (ex. pilule de recherche de l'accueil)85 .onReceive(NotificationCenter.default.publisher(for: .kaSwitchTab)) { note in86 if let i = note.object as? Int, (0..<6).contains(i) {87 withAnimation(.spring(response: 0.35, dampingFraction: 0.8)) { tab = i }88 }89 }90 .task { await recents.refreshAlerts() }91 .sheet(isPresented: $showAgent) {92 AgentChatView()93 .presentationDetents([.large])94 }95 }9697 @ViewBuilder98 private func pane(_ i: Int, @ViewBuilder content: () -> some View) -> some View {99 if loaded.contains(i) {100 content()101 .opacity(tab == i ? 1 : 0)102 .allowsHitTesting(tab == i)103 .accessibilityHidden(tab != i)104 }105 }106}107108extension Notification.Name {109 /// Demande de changement d'onglet (object = index Int)110 static let kaSwitchTab = Notification.Name("ka.switch.tab")111}112113/// Onglet Carte : la carte unifiée directement (pas de fermeture — c'est un onglet)114struct MapTab: View {115 var body: some View {116 UnifiedMapView(embedded: true)117 }118}119120/// Onglet Profil — inclut Favoris, Historique et Recherches sauvegardées.121struct MoreTab: View {122 var body: some View {123 ProfileView()124 }125}126