SplashScreenView.swift 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. import SwiftUI
  2. struct SplashScreenView: View {
  3. @Bindable var viewModel: SplashViewModel
  4. @Environment(AppearanceManager.self) private var appearanceManager
  5. @State private var contentScale: CGFloat = 0.9
  6. @State private var contentOpacity: Double = 0
  7. @State private var detailsOpacity: Double = 0
  8. @State private var glowPulse = false
  9. var body: some View {
  10. let isDark = appearanceManager.isDarkMode
  11. ZStack {
  12. background(isDark: isDark)
  13. glowOrb(isDark: isDark)
  14. VStack(spacing: 28) {
  15. brandMark
  16. VStack(spacing: 24) {
  17. VStack(spacing: 10) {
  18. title
  19. Text("AI Assistant for Reddit")
  20. .font(.system(size: 14, weight: .medium))
  21. .foregroundStyle(AppTheme.textSecondary)
  22. }
  23. loadingBar
  24. }
  25. .opacity(detailsOpacity)
  26. }
  27. .frame(maxWidth: .infinity, maxHeight: .infinity)
  28. .scaleEffect(contentScale)
  29. .opacity(contentOpacity)
  30. }
  31. .frame(maxWidth: .infinity, maxHeight: .infinity)
  32. .ignoresSafeArea()
  33. .onAppear {
  34. withAnimation(.spring(response: 0.62, dampingFraction: 0.78)) {
  35. contentScale = 1
  36. contentOpacity = 1
  37. }
  38. withAnimation(.easeOut(duration: 0.55).delay(0.12)) {
  39. detailsOpacity = 1
  40. }
  41. withAnimation(.easeInOut(duration: 2.4).repeatForever(autoreverses: true)) {
  42. glowPulse = true
  43. }
  44. }
  45. }
  46. private func background(isDark: Bool) -> some View {
  47. Group {
  48. if isDark {
  49. AppTheme.background
  50. } else {
  51. LinearGradient(
  52. colors: [
  53. Color(hex: 0xF3F0FF),
  54. AppTheme.background
  55. ],
  56. startPoint: .top,
  57. endPoint: .bottom
  58. )
  59. }
  60. }
  61. .ignoresSafeArea()
  62. }
  63. private func glowOrb(isDark: Bool) -> some View {
  64. Circle()
  65. .fill(
  66. RadialGradient(
  67. colors: [
  68. AppTheme.accentPurple.opacity(isDark ? 0.22 : 0.14),
  69. Color.clear
  70. ],
  71. center: .center,
  72. startRadius: 0,
  73. endRadius: 280
  74. )
  75. )
  76. .frame(width: 560, height: 560)
  77. .scaleEffect(glowPulse ? 1.06 : 0.94)
  78. .offset(x: 180, y: -220)
  79. .allowsHitTesting(false)
  80. }
  81. private var brandMark: some View {
  82. Image("AppLogo")
  83. .resizable()
  84. .interpolation(.high)
  85. .scaledToFit()
  86. .frame(width: 80, height: 80)
  87. .shadow(color: Color(hex: 0xFF4500).opacity(0.35), radius: 28, y: 10)
  88. }
  89. private var loadingBar: some View {
  90. VStack(spacing: 12) {
  91. GeometryReader { geometry in
  92. ZStack(alignment: .leading) {
  93. Capsule()
  94. .fill(AppTheme.border)
  95. Capsule()
  96. .fill(
  97. LinearGradient(
  98. colors: [AppTheme.accentPurple, AppTheme.accentPurpleLight],
  99. startPoint: .leading,
  100. endPoint: .trailing
  101. )
  102. )
  103. .frame(width: max(geometry.size.width * viewModel.loadingProgress, geometry.size.height))
  104. }
  105. }
  106. .frame(width: 300, height: 7)
  107. Text("Loading…")
  108. .font(.system(size: 12, weight: .medium))
  109. .foregroundStyle(AppTheme.textTertiary)
  110. }
  111. .frame(maxWidth: .infinity)
  112. }
  113. private var title: some View {
  114. Text(AppLinks.appName)
  115. .font(.system(size: 34, weight: .bold))
  116. .tracking(-0.6)
  117. .foregroundStyle(AppTheme.textPrimary)
  118. }
  119. }