Andromida/Andromida/App/Views/Onboarding/WelcomeStepView.swift

147 lines
4.9 KiB
Swift

import SwiftUI
import Bedrock
/// The welcome screen shown as the first step of the setup wizard.
struct WelcomeStepView: View {
let onContinue: () -> Void
@State private var animateRings = false
@State private var animateText = false
@State private var animateButton = false
var body: some View {
VStack(spacing: Design.Spacing.xxLarge) {
Spacer()
// Animated rings visual
animatedRingsView
.frame(width: 200, height: 200)
.opacity(animateRings ? 1 : 0)
.scaleEffect(animateRings ? 1 : 0.8)
VStack(spacing: Design.Spacing.medium) {
Text(String(localized: "Welcome to Rituals"))
.typography(.heroBold)
.foregroundStyle(AppTextColors.primary)
.multilineTextAlignment(.center)
Text(String(localized: "Build lasting habits through focused, time-bound journeys"))
.typography(.body)
.foregroundStyle(AppTextColors.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal, Design.Spacing.xxLarge)
}
.opacity(animateText ? 1 : 0)
.offset(y: animateText ? 0 : 20)
Spacer()
// Get Started button
Button(action: onContinue) {
Text(String(localized: "Get Started"))
.typography(.heading)
.foregroundStyle(AppTextColors.inverse)
.frame(maxWidth: .infinity)
.frame(height: AppMetrics.Size.buttonHeight)
.background(AppAccent.primary)
.clipShape(.rect(cornerRadius: Design.CornerRadius.medium))
}
.accessibilityIdentifier("onboarding.getStarted")
.padding(.horizontal, Design.Spacing.xxLarge)
.opacity(animateButton ? 1 : 0)
.offset(y: animateButton ? 0 : 20)
Spacer()
.frame(height: Design.Spacing.xxLarge)
}
.onAppear {
startAnimations()
}
}
// MARK: - Animated Rings
private var animatedRingsView: some View {
ZStack {
// Outer ring
Circle()
.stroke(
AppAccent.primary.opacity(0.3),
style: StrokeStyle(lineWidth: 8, lineCap: .round)
)
.frame(width: 180, height: 180)
.rotationEffect(.degrees(animateRings ? 360 : 0))
.animation(
.linear(duration: 20).repeatForever(autoreverses: false),
value: animateRings
)
// Middle ring with arc
Circle()
.trim(from: 0, to: 0.7)
.stroke(
AppAccent.primary.opacity(0.6),
style: StrokeStyle(lineWidth: 10, lineCap: .round)
)
.frame(width: 140, height: 140)
.rotationEffect(.degrees(animateRings ? -360 : 0))
.animation(
.linear(duration: 15).repeatForever(autoreverses: false),
value: animateRings
)
// Inner ring with arc
Circle()
.trim(from: 0, to: 0.5)
.stroke(
AppAccent.primary,
style: StrokeStyle(lineWidth: 12, lineCap: .round)
)
.frame(width: 100, height: 100)
.rotationEffect(.degrees(animateRings ? 360 : 0))
.animation(
.linear(duration: 10).repeatForever(autoreverses: false),
value: animateRings
)
// Center icon
SymbolIcon("sparkles", size: .card, color: AppAccent.primary)
.scaleEffect(animateRings ? 1.1 : 1.0)
.animation(
.easeInOut(duration: 1.5).repeatForever(autoreverses: true),
value: animateRings
)
}
}
// MARK: - Animations
private func startAnimations() {
// Stagger the animations
withAnimation(.easeOut(duration: 0.6)) {
animateRings = true
}
withAnimation(.easeOut(duration: 0.6).delay(0.3)) {
animateText = true
}
withAnimation(.easeOut(duration: 0.6).delay(0.6)) {
animateButton = true
}
}
}
#Preview {
ZStack {
LinearGradient(
colors: [AppSurface.primary, AppSurface.secondary],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
.ignoresSafeArea()
WelcomeStepView(onContinue: {})
}
}