Files
mobile-ios/Rosetta/RosettaApp.swift
senseiGai e26d94b268 Исправление бесконечного рендер-цикла SearchView и поиск по публичному ключу
- SearchViewModel: заменён @Observable на ObservableObject + @Published
  (устранён infinite body loop SearchView → 99% CPU фриз после логина)
- SearchView: @State → @StateObject, RecentSection: @ObservedObject
- Добавлен клиентский поиск по публичному ключу (сервер ищет только по нику)
- ChatDetailView: убран @State на DialogRepository singleton
- ChatListView: замена closure на @Binding, убран DispatchQueue.main.async
- MainTabView: убран пустой onChange, замена closure на @Binding
- SettingsViewModel: конвертирован в ObservableObject
- Добавлены debug-принты для отладки рендер-циклов

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 05:14:54 +05:00

124 lines
3.7 KiB
Swift

import SwiftUI
// MARK: - App State
private enum AppState {
case splash
case onboarding
case auth
case unlock
case main
}
// MARK: - RosettaApp
@main
struct RosettaApp: App {
init() {
UIWindow.appearance().backgroundColor = .black
// Detect fresh install: UserDefaults are wiped on uninstall, Keychain is not.
// If this is the first launch after install, clear any stale Keychain data.
if !UserDefaults.standard.bool(forKey: "hasLaunchedBefore") {
try? AccountManager.shared.deleteAccount()
UserDefaults.standard.set(true, forKey: "hasLaunchedBefore")
}
// Avoid heavy startup work on MainActor; Lottie assets load lazily on first use.
}
@AppStorage("hasCompletedOnboarding") private var hasCompletedOnboarding = false
@AppStorage("isLoggedIn") private var isLoggedIn = false
@AppStorage("hasLaunchedBefore") private var hasLaunchedBefore = false
@State private var appState: AppState = .splash
var body: some Scene {
WindowGroup {
ZStack {
Color.black
.ignoresSafeArea()
rootView
.transition(.opacity.animation(.easeInOut(duration: 0.5)))
}
.preferredColorScheme(.dark)
}
}
@MainActor static var _bodyCount = 0
@ViewBuilder
private var rootView: some View {
let _ = Self._bodyCount += 1
let _ = print("⭐ RosettaApp.rootView #\(Self._bodyCount) state=\(appState)")
switch appState {
case .splash:
SplashView {
withAnimation(.easeInOut(duration: 0.55)) {
determineNextState()
}
}
case .onboarding:
OnboardingView {
withAnimation(.easeInOut(duration: 0.55)) {
hasCompletedOnboarding = true
appState = .auth
}
}
case .auth:
AuthCoordinator(
onAuthComplete: {
withAnimation(.easeInOut(duration: 0.55)) {
isLoggedIn = true
appState = .main
}
},
onBackToUnlock: AccountManager.shared.hasAccount ? {
// Go back to unlock screen if an account exists
withAnimation(.easeInOut(duration: 0.55)) {
appState = .unlock
}
} : nil
)
case .unlock:
UnlockView(
onUnlocked: {
withAnimation(.easeInOut(duration: 0.55)) {
isLoggedIn = true
appState = .main
}
},
onCreateNewAccount: {
// Go to auth flow (Welcome screen with back button)
// Does NOT delete the old account Android keeps multiple accounts
withAnimation(.easeInOut(duration: 0.55)) {
appState = .auth
}
}
)
case .main:
MainTabView(onLogout: {
withAnimation(.easeInOut(duration: 0.55)) {
isLoggedIn = false
appState = .unlock
}
})
}
}
private func determineNextState() {
if AccountManager.shared.hasAccount {
// Existing user unlock with password
appState = .unlock
} else {
// No account always show onboarding first, then auth
hasCompletedOnboarding = false
appState = .onboarding
}
}
}