- 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>
124 lines
3.7 KiB
Swift
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
|
|
}
|
|
}
|
|
}
|