Files
Reflect/Shared/Views/SettingsView/DebugAnimationSettingsView.swift
Trey T ed8205cd88 Complete accessibility identifier coverage across all 152 project files
Exhaustive file-by-file audit of every Swift file in the project (iOS app,
Watch app, Widget extension). Every interactive UI element — buttons, toggles,
pickers, links, menus, tap gestures, text editors, color pickers, photo
pickers — now has an accessibilityIdentifier for XCUITest automation.

46 files changed across Shared/, Onboarding/, Watch App/, and Widget targets.
Added ~100 new ID definitions covering settings debug controls, export/photo
views, sharing templates, customization subviews, onboarding flows, tip
modals, widget voting buttons, and watch mood buttons.
2026-03-26 08:34:56 -05:00

360 lines
12 KiB
Swift

//
// DebugAnimationSettingsView.swift
// Reflect
//
// Debug-only view for experimenting with vote celebration animations.
//
import SwiftUI
// MARK: - Main Debug View
struct DebugAnimationSettingsView: View {
@Environment(\.dismiss) private var dismiss
@Environment(\.colorScheme) private var colorScheme
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
private var textColor: Color { theme.currentTheme.labelColor }
@State private var selectedAnimation: CelebrationAnimationType = .vortexCheckmark
@State private var isAnimating = false
private var isDark: Bool { colorScheme == .dark }
var body: some View {
VStack(spacing: 0) {
// Lab Header
labHeader
.padding(.top, 8)
// Animation Picker - Horizontal Scroll Cards
animationPicker
.padding(.top, 16)
// Preview Area
previewArea
.padding(.top, 20)
.padding(.horizontal, 16)
Spacer()
}
.background(
ZStack {
theme.currentTheme.bg
// Subtle grid pattern for "lab" feel
GridPatternView()
.opacity(isDark ? 0.03 : 0.02)
}
.ignoresSafeArea()
)
.navigationTitle("")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Done") {
dismiss()
}
.accessibilityIdentifier(AccessibilityID.Debug.animationDoneButton)
}
}
}
// MARK: - Lab Header
private var labHeader: some View {
VStack(spacing: 6) {
HStack(spacing: 8) {
Image(systemName: "flask.fill")
.font(.title2)
.foregroundStyle(
LinearGradient(
colors: [.purple, .pink],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
Text("Animation Lab")
.font(.system(size: 24, weight: .bold, design: .rounded))
.foregroundColor(textColor)
}
Text("Experiment with vote celebrations")
.font(.system(size: 14, weight: .medium, design: .rounded))
.foregroundColor(.secondary)
}
}
// MARK: - Animation Picker
private var animationPicker: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 12) {
ForEach(CelebrationAnimationType.allCases) { type in
AnimationCard(
type: type,
isSelected: selectedAnimation == type,
isDark: isDark
) {
withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
selectedAnimation = type
}
}
}
}
.padding(.horizontal, 16)
.padding(.vertical, 8)
}
}
// MARK: - Preview Area
private var previewArea: some View {
VStack(spacing: 12) {
// Current selection indicator
HStack(spacing: 6) {
Circle()
.fill(selectedAnimation.accentColor)
.frame(width: 8, height: 8)
Text(selectedAnimation.rawValue)
.font(.system(size: 13, weight: .semibold, design: .monospaced))
.foregroundColor(selectedAnimation.accentColor)
Text("")
.foregroundColor(.secondary.opacity(0.5))
Text(selectedAnimation.description)
.font(.system(size: 13, weight: .medium, design: .rounded))
.foregroundColor(.secondary)
}
// The animated voting view
AnimatedVotingView(
animationType: selectedAnimation,
isAnimating: $isAnimating
)
.frame(height: 280)
.background(
RoundedRectangle(cornerRadius: 24)
.fill(isDark ? Color(.systemGray6) : .white)
.shadow(color: .black.opacity(isDark ? 0.3 : 0.08), radius: 20, x: 0, y: 8)
)
.overlay(
RoundedRectangle(cornerRadius: 24)
.stroke(selectedAnimation.accentColor.opacity(0.3), lineWidth: 1)
)
.clipShape(RoundedRectangle(cornerRadius: 24))
}
}
}
// MARK: - Grid Pattern Background
struct GridPatternView: View {
var body: some View {
Canvas { context, size in
let gridSize: CGFloat = 20
for x in stride(from: 0, to: size.width, by: gridSize) {
for y in stride(from: 0, to: size.height, by: gridSize) {
let rect = CGRect(x: x, y: y, width: 1, height: 1)
context.fill(Path(ellipseIn: rect), with: .color(.primary))
}
}
}
}
}
// MARK: - Animation Card
struct AnimationCard: View {
let type: CelebrationAnimationType
let isSelected: Bool
let isDark: Bool
let onTap: () -> Void
@State private var isPressed = false
var body: some View {
Button(action: onTap) {
VStack(spacing: 8) {
// Icon
ZStack {
Circle()
.fill(
isSelected
? type.accentColor.opacity(0.2)
: (isDark ? Color(.systemGray5) : Color(.systemGray6))
)
.frame(width: 44, height: 44)
Image(systemName: type.icon)
.font(.system(size: 20, weight: .medium))
.foregroundColor(isSelected ? type.accentColor : .secondary)
}
// Name
Text(type.rawValue)
.font(.system(size: 11, weight: .semibold, design: .rounded))
.foregroundColor(isSelected ? type.accentColor : .secondary)
}
.frame(width: 72, height: 80)
.background(
RoundedRectangle(cornerRadius: 16)
.fill(isDark ? Color(.systemGray6) : .white)
.shadow(
color: isSelected ? type.accentColor.opacity(0.3) : .black.opacity(0.05),
radius: isSelected ? 8 : 4,
x: 0,
y: isSelected ? 4 : 2
)
)
.overlay(
RoundedRectangle(cornerRadius: 16)
.stroke(
isSelected ? type.accentColor : Color.clear,
lineWidth: 2
)
)
.scaleEffect(isPressed ? 0.95 : (isSelected ? 1.02 : 1.0))
}
.accessibilityIdentifier(AccessibilityID.Debug.animationCard(type.rawValue))
.buttonStyle(PlainButtonStyle())
.onLongPressGesture(minimumDuration: .infinity, pressing: { pressing in
withAnimation(.easeInOut(duration: 0.15)) {
isPressed = pressing
}
}, perform: {})
}
}
// MARK: - Animated Voting View
struct AnimatedVotingView: View {
let animationType: CelebrationAnimationType
@Binding var isAnimating: Bool
@State private var selectedMood: Mood = .great
// Animation state
@State private var animationScale: CGFloat = 1
@State private var animationRotation: Angle = .zero
@State private var animationOffset: CGSize = .zero
var body: some View {
ZStack {
// The voting content (gets animated)
DebugVotingContentView(selectedMood: $selectedMood) {
triggerAnimation()
}
.opacity(isAnimating ? 0 : 1)
.scaleEffect(animationScale)
.rotation3DEffect(animationRotation, axis: (x: 0, y: 1, z: 0))
.offset(animationOffset)
// Animation overlay
if isAnimating {
CelebrationOverlayView(
animationType: animationType,
mood: selectedMood
) {
// Reset handled by triggerAnimation
}
}
}
.animation(.spring(response: 0.5, dampingFraction: 0.7), value: isAnimating)
}
private func triggerAnimation() {
guard !isAnimating else { return }
let impact = UIImpactFeedbackGenerator(style: .medium)
impact.impactOccurred()
// Set animation properties based on type
withAnimation(.easeInOut(duration: 0.4)) {
isAnimating = true
switch animationType {
case .vortexCheckmark:
animationScale = 0.01
case .explosionReveal:
animationScale = 2.5
case .flipReveal:
animationRotation = .degrees(90)
case .shatterReform:
break
case .pulseWave:
animationScale = 0.9
case .fireworks:
animationScale = 0.8
case .confettiCannon:
animationScale = 0.95
case .morphBlob:
animationScale = 0.5
case .zoomTunnel:
animationScale = 0.01
case .gravityDrop:
animationOffset = CGSize(width: 0, height: 400)
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + animationType.duration) {
withAnimation(.easeOut(duration: 0.3)) {
isAnimating = false
animationScale = 1
animationRotation = .zero
animationOffset = .zero
}
}
}
}
// MARK: - Debug Voting Content View
struct DebugVotingContentView: View {
@Binding var selectedMood: Mood
let onVote: () -> Void
var body: some View {
VStack(spacing: 20) {
Text("How are you feeling?")
.font(.system(size: 18, weight: .semibold, design: .rounded))
.foregroundColor(.primary)
HStack(spacing: 12) {
ForEach(Mood.allValues, id: \.self) { mood in
Button {
selectedMood = mood
onVote()
} label: {
mood.icon
.resizable()
.scaledToFit()
.frame(width: 40, height: 40)
.padding(8)
.background(
Circle()
.fill(mood.color.opacity(0.15))
)
}
.accessibilityIdentifier(AccessibilityID.Debug.debugMoodButton(mood.strValue))
}
}
Text("Tap to vote")
.font(.system(size: 12, weight: .medium, design: .rounded))
.foregroundColor(.secondary)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
// MARK: - Preview
#Preview {
NavigationStack {
DebugAnimationSettingsView()
}
}