a8a9ad64f3
- Imports de CoreData que faltaban (MemberImportVisibility) en AccountsView, ImportDataView, ChartsContainerView y AddSourceView. - Aislamiento MainActor: IsolatedDefaultValues en el target de la app, LinearGradient @MainActor, WatchSyncMessage nonisolated, closures de ReviewPromptService/MonthlyCheckInStore/AppIntents. - Warnings de valores sin usar (SnapshotGapDetector, DashboardLayoutStore, OnboardingView, GoalEditorView, CoreDataStack, SettingsViewModel). - foregroundColor→foregroundStyle, cornerRadius→clipShape, navigationBarLeading/Trailing→topBarLeading/Trailing, Task.sleep(nanoseconds:)→sleep(for:), NavigationLink(isActive:)→ navigationDestination(isPresented:). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F1u4K16xy7eQVtgsYNZ9Vn
290 lines
8.0 KiB
Swift
290 lines
8.0 KiB
Swift
import SwiftUI
|
|
|
|
struct LoadingView: View {
|
|
var message: String = "Loading..."
|
|
|
|
var body: some View {
|
|
VStack(spacing: 16) {
|
|
ProgressView()
|
|
.scaleEffect(1.5)
|
|
|
|
Text(message)
|
|
.font(.subheadline)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
.background(Color(.systemBackground))
|
|
}
|
|
}
|
|
|
|
struct AppLaunchLoadingView: View {
|
|
var messageKey: LocalizedStringKey = "loading"
|
|
|
|
var body: some View {
|
|
VStack(spacing: 20) {
|
|
Spacer()
|
|
|
|
Image("BrandMark")
|
|
.resizable()
|
|
.scaledToFit()
|
|
.frame(width: 140, height: 140)
|
|
.padding(16)
|
|
.background(Color.appPrimary.opacity(0.08))
|
|
.clipShape(RoundedRectangle(cornerRadius: 28))
|
|
|
|
ProgressView()
|
|
.scaleEffect(1.2)
|
|
|
|
Text(messageKey)
|
|
.font(.subheadline)
|
|
.foregroundStyle(.secondary)
|
|
|
|
Spacer()
|
|
}
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
.background(AppBackground())
|
|
}
|
|
}
|
|
|
|
// MARK: - Skeleton Loading
|
|
|
|
struct SkeletonView: View {
|
|
@State private var isAnimating = false
|
|
|
|
var body: some View {
|
|
Rectangle()
|
|
.fill(
|
|
LinearGradient(
|
|
colors: [
|
|
Color.gray.opacity(0.1),
|
|
Color.gray.opacity(0.2),
|
|
Color.gray.opacity(0.1)
|
|
],
|
|
startPoint: .leading,
|
|
endPoint: .trailing
|
|
)
|
|
)
|
|
.offset(x: isAnimating ? 200 : -200)
|
|
.animation(
|
|
Animation.linear(duration: 1.5)
|
|
.repeatForever(autoreverses: false),
|
|
value: isAnimating
|
|
)
|
|
.mask(Rectangle())
|
|
.onAppear {
|
|
isAnimating = true
|
|
}
|
|
}
|
|
}
|
|
|
|
struct SkeletonCardView: View {
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
SkeletonView()
|
|
.frame(width: 100, height: 16)
|
|
.clipShape(RoundedRectangle(cornerRadius: 4))
|
|
|
|
SkeletonView()
|
|
.frame(height: 32)
|
|
.clipShape(RoundedRectangle(cornerRadius: 4))
|
|
|
|
HStack {
|
|
SkeletonView()
|
|
.frame(width: 80, height: 14)
|
|
.clipShape(RoundedRectangle(cornerRadius: 4))
|
|
|
|
Spacer()
|
|
|
|
SkeletonView()
|
|
.frame(width: 60, height: 14)
|
|
.clipShape(RoundedRectangle(cornerRadius: 4))
|
|
}
|
|
}
|
|
.padding()
|
|
.background(Color(.systemBackground))
|
|
.clipShape(RoundedRectangle(cornerRadius: AppConstants.UI.cornerRadius))
|
|
}
|
|
}
|
|
|
|
// MARK: - Empty State View
|
|
|
|
struct EmptyStateView: View {
|
|
let icon: String
|
|
let title: String
|
|
let message: String
|
|
var actionTitle: String?
|
|
var action: (() -> Void)?
|
|
|
|
var body: some View {
|
|
VStack(spacing: 20) {
|
|
Image(systemName: icon)
|
|
.font(.system(size: 60))
|
|
.foregroundStyle(.secondary)
|
|
|
|
Text(title)
|
|
.font(.title2.weight(.semibold))
|
|
|
|
Text(message)
|
|
.font(.subheadline)
|
|
.foregroundStyle(.secondary)
|
|
.multilineTextAlignment(.center)
|
|
.padding(.horizontal, 40)
|
|
|
|
if let actionTitle = actionTitle, let action = action {
|
|
Button(action: action) {
|
|
Text(actionTitle)
|
|
.font(.headline)
|
|
.foregroundStyle(.white)
|
|
.padding()
|
|
.frame(maxWidth: 200)
|
|
.background(Color.appPrimary)
|
|
.clipShape(RoundedRectangle(cornerRadius: AppConstants.UI.cornerRadius))
|
|
}
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
}
|
|
}
|
|
|
|
// MARK: - Error View
|
|
|
|
struct ErrorView: View {
|
|
let message: String
|
|
var retryAction: (() -> Void)?
|
|
|
|
var body: some View {
|
|
VStack(spacing: 16) {
|
|
Image(systemName: "exclamationmark.triangle.fill")
|
|
.font(.system(size: 48))
|
|
.foregroundStyle(Color.appWarning)
|
|
|
|
Text("Something went wrong")
|
|
.font(.headline)
|
|
|
|
Text(message)
|
|
.font(.subheadline)
|
|
.foregroundStyle(.secondary)
|
|
.multilineTextAlignment(.center)
|
|
.padding(.horizontal, 40)
|
|
|
|
if let retryAction = retryAction {
|
|
Button(action: retryAction) {
|
|
Label("Try Again", systemImage: "arrow.clockwise")
|
|
.font(.subheadline.weight(.semibold))
|
|
.padding(.horizontal, 20)
|
|
.padding(.vertical, 10)
|
|
.background(Color.appPrimary)
|
|
.foregroundStyle(.white)
|
|
.clipShape(RoundedRectangle(cornerRadius: 20))
|
|
}
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
}
|
|
}
|
|
|
|
// MARK: - Success View
|
|
|
|
struct SuccessView: View {
|
|
let title: String
|
|
let message: String
|
|
var action: (() -> Void)?
|
|
|
|
var body: some View {
|
|
VStack(spacing: 20) {
|
|
ZStack {
|
|
Circle()
|
|
.fill(Color.positiveGreen.opacity(0.1))
|
|
.frame(width: 100, height: 100)
|
|
|
|
Image(systemName: "checkmark.circle.fill")
|
|
.font(.system(size: 60))
|
|
.foregroundStyle(Color.positiveGreen)
|
|
}
|
|
|
|
Text(title)
|
|
.font(.title2.weight(.bold))
|
|
|
|
Text(message)
|
|
.font(.subheadline)
|
|
.foregroundStyle(.secondary)
|
|
.multilineTextAlignment(.center)
|
|
.padding(.horizontal, 40)
|
|
|
|
if let action = action {
|
|
Button(action: action) {
|
|
Text("Continue")
|
|
.font(.headline)
|
|
.foregroundStyle(.white)
|
|
.padding()
|
|
.frame(maxWidth: 200)
|
|
.background(Color.appPrimary)
|
|
.clipShape(RoundedRectangle(cornerRadius: AppConstants.UI.cornerRadius))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Toast View
|
|
|
|
struct ToastView: View {
|
|
enum ToastType {
|
|
case success, error, info
|
|
|
|
var icon: String {
|
|
switch self {
|
|
case .success: return "checkmark.circle.fill"
|
|
case .error: return "xmark.circle.fill"
|
|
case .info: return "info.circle.fill"
|
|
}
|
|
}
|
|
|
|
var color: Color {
|
|
switch self {
|
|
case .success: return .positiveGreen
|
|
case .error: return .negativeRed
|
|
case .info: return .appPrimary
|
|
}
|
|
}
|
|
}
|
|
|
|
let message: String
|
|
let type: ToastType
|
|
|
|
var body: some View {
|
|
HStack(spacing: 12) {
|
|
Image(systemName: type.icon)
|
|
.foregroundStyle(type.color)
|
|
|
|
Text(message)
|
|
.font(.subheadline)
|
|
}
|
|
.padding()
|
|
.background(Color(.systemBackground))
|
|
.clipShape(RoundedRectangle(cornerRadius: AppConstants.UI.cornerRadius))
|
|
.shadow(color: .black.opacity(0.1), radius: 10, y: 5)
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
VStack(spacing: 20) {
|
|
LoadingView()
|
|
.frame(height: 100)
|
|
|
|
SkeletonCardView()
|
|
|
|
EmptyStateView(
|
|
icon: "tray",
|
|
title: "No Data",
|
|
message: "Start adding your investments to see them here.",
|
|
actionTitle: "Get Started",
|
|
action: {}
|
|
)
|
|
.frame(height: 300)
|
|
|
|
ToastView(message: "Successfully saved!", type: .success)
|
|
}
|
|
.padding()
|
|
}
|