Files
Reflect/Shared/Views/LockScreenView.swift
Trey T e7648ddd8a Add missing accessibility identifiers to all interactive UI elements
Audit found ~50+ interactive elements (buttons, toggles, pickers, alerts,
links) missing accessibility identifiers across 13 view files. Added
centralized ID definitions and applied them to every entry detail button,
guided reflection control, settings toggle, paywall unlock button,
subscription/IAP button, lock screen control, and photo action dialog.
2026-03-26 07:59:52 -05:00

2046 lines
65 KiB
Swift

//
// LockScreenView.swift
// Reflect
//
// Lock screen shown when privacy lock is enabled and app needs authentication.
// Supports multiple themed styles that match app themes.
//
import SwiftUI
// MARK: - Lock Screen Style Protocol
protocol LockScreenTheme {
var backgroundColor: AnyView { get }
var centralElement: AnyView { get }
var titleText: String { get }
var subtitleText: String { get }
var taglineText: String { get }
var titleFont: Font { get }
var subtitleFont: Font { get }
var taglineFont: Font { get }
func titleColor(isDark: Bool) -> Color
func subtitleColor(isDark: Bool) -> Color
func taglineColor(isDark: Bool) -> Color
func buttonStyle(isDark: Bool) -> LockButtonStyle
}
struct LockButtonStyle {
let backgroundColor: Color
let foregroundColor: Color
let borderColor: Color
let useMaterial: Bool
}
// MARK: - Floating Mood Particle
struct MoodParticle: Identifiable {
let id = UUID()
var x: CGFloat
var y: CGFloat
let size: CGFloat
let color: Color
let duration: Double
let delay: Double
}
// MARK: - Aurora Gradient Background
struct AuroraBackground: View {
@Environment(\.colorScheme) private var colorScheme
@State private var animateGradient = false
private let moodColors: [Color] = [
Color(hex: "31d158"), // great - green
Color(hex: "ffd709"), // good - yellow
Color(hex: "0b84ff"), // average - blue
Color(hex: "ff9e0b"), // bad - orange
Color(hex: "ff453a"), // horrible - red
]
private var isDark: Bool { colorScheme == .dark }
var body: some View {
ZStack {
// Base gradient - adapts to color scheme
LinearGradient(
colors: isDark ? [
Color(hex: "0a0a0f"),
Color(hex: "12121a"),
Color(hex: "0d0d14")
] : [
Color(hex: "f8f9fa"),
Color(hex: "e9ecef"),
Color(hex: "f1f3f5")
],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
// Aurora layer 1 - green/blue
EllipticalGradient(
colors: [
moodColors[0].opacity(isDark ? 0.3 : 0.2),
moodColors[2].opacity(isDark ? 0.15 : 0.1),
.clear
],
center: .topLeading,
startRadiusFraction: 0,
endRadiusFraction: 0.8
)
.blur(radius: 60)
.offset(x: animateGradient ? 20 : -20, y: animateGradient ? -30 : 30)
// Aurora layer 2 - yellow/orange
EllipticalGradient(
colors: [
moodColors[1].opacity(isDark ? 0.2 : 0.15),
moodColors[3].opacity(isDark ? 0.1 : 0.08),
.clear
],
center: .bottomTrailing,
startRadiusFraction: 0,
endRadiusFraction: 0.7
)
.blur(radius: 80)
.offset(x: animateGradient ? -30 : 30, y: animateGradient ? 20 : -20)
// Aurora layer 3 - subtle red accent
RadialGradient(
colors: [
moodColors[4].opacity(isDark ? 0.15 : 0.1),
.clear
],
center: UnitPoint(x: 0.8, y: 0.3),
startRadius: 0,
endRadius: 200
)
.blur(radius: 40)
.offset(y: animateGradient ? -10 : 10)
}
.ignoresSafeArea()
.onAppear {
withAnimation(
.easeInOut(duration: 8)
.repeatForever(autoreverses: true)
) {
animateGradient = true
}
}
}
}
// MARK: - Floating Particles Layer
struct FloatingParticlesView: View {
@State private var particles: [MoodParticle] = []
private let moodColors: [Color] = [
Color(hex: "31d158"),
Color(hex: "ffd709"),
Color(hex: "0b84ff"),
Color(hex: "ff9e0b"),
Color(hex: "ff453a"),
]
var body: some View {
GeometryReader { geo in
ZStack {
ForEach(particles) { particle in
Circle()
.fill(particle.color)
.frame(width: particle.size, height: particle.size)
.blur(radius: particle.size * 0.3)
.position(x: particle.x, y: particle.y)
.modifier(FloatingAnimation(
startY: particle.y,
duration: particle.duration,
delay: particle.delay
))
}
}
.onAppear {
generateParticles(in: geo.size)
}
}
}
private func generateParticles(in size: CGSize) {
particles = (0..<15).map { _ in
MoodParticle(
x: CGFloat.random(in: 0...size.width),
y: CGFloat.random(in: 0...size.height),
size: CGFloat.random(in: 4...12),
color: moodColors.randomElement()!.opacity(Double.random(in: 0.2...0.5)),
duration: Double.random(in: 6...12),
delay: Double.random(in: 0...3)
)
}
}
}
struct FloatingAnimation: ViewModifier {
let startY: CGFloat
let duration: Double
let delay: Double
@State private var offset: CGFloat = 0
func body(content: Content) -> some View {
content
.offset(y: offset)
.onAppear {
withAnimation(
.easeInOut(duration: duration)
.repeatForever(autoreverses: true)
.delay(delay)
) {
offset = CGFloat.random(in: -30...30)
}
}
}
}
// MARK: - Central Breathing Orb
struct BreathingOrb: View {
@Environment(\.colorScheme) private var colorScheme
@State private var breathe = false
@State private var rotate = false
private let moodColors: [Color] = [
Color(hex: "31d158"),
Color(hex: "ffd709"),
Color(hex: "0b84ff"),
Color(hex: "ff9e0b"),
Color(hex: "ff453a"),
]
private var isDark: Bool { colorScheme == .dark }
var body: some View {
ZStack {
// Outer glow
Circle()
.fill(
AngularGradient(
colors: moodColors + [moodColors[0]],
center: .center,
startAngle: .degrees(0),
endAngle: .degrees(360)
)
)
.frame(width: 180, height: 180)
.blur(radius: 40)
.opacity(isDark ? 0.6 : 0.5)
.scaleEffect(breathe ? 1.2 : 0.9)
.rotationEffect(.degrees(rotate ? 360 : 0))
// Middle ring
Circle()
.fill(
AngularGradient(
colors: moodColors.reversed() + [moodColors.last!],
center: .center
)
)
.frame(width: 120, height: 120)
.blur(radius: 20)
.opacity(isDark ? 0.8 : 0.6)
.scaleEffect(breathe ? 1.1 : 0.95)
.rotationEffect(.degrees(rotate ? -360 : 0))
// Inner core
Circle()
.fill(
RadialGradient(
colors: isDark ? [
.white.opacity(0.9),
.white.opacity(0.3),
.clear
] : [
.white,
.white.opacity(0.6),
.clear
],
center: .center,
startRadius: 0,
endRadius: 40
)
)
.frame(width: 80, height: 80)
.scaleEffect(breathe ? 1.05 : 0.98)
.shadow(color: .black.opacity(isDark ? 0 : 0.1), radius: 10)
// Glossy highlight
Ellipse()
.fill(
LinearGradient(
colors: [.white.opacity(isDark ? 0.4 : 0.8), .clear],
startPoint: .top,
endPoint: .center
)
)
.frame(width: 50, height: 30)
.offset(y: -15)
.scaleEffect(breathe ? 1.05 : 0.98)
}
.onAppear {
withAnimation(
.easeInOut(duration: 4)
.repeatForever(autoreverses: true)
) {
breathe = true
}
withAnimation(
.linear(duration: 20)
.repeatForever(autoreverses: false)
) {
rotate = true
}
}
}
}
// MARK: - Zen Lock Screen Theme
struct ZenLockBackground: View {
@Environment(\.colorScheme) private var colorScheme
@State private var breathe = false
var body: some View {
ZStack {
// Warm paper background
LinearGradient(
colors: colorScheme == .dark ? [
Color(red: 0.12, green: 0.11, blue: 0.10),
Color(red: 0.08, green: 0.07, blue: 0.06)
] : [
Color(red: 0.96, green: 0.94, blue: 0.90),
Color(red: 0.92, green: 0.90, blue: 0.86)
],
startPoint: .top,
endPoint: .bottom
)
// Subtle ink wash effect
Circle()
.fill(
RadialGradient(
colors: colorScheme == .dark ? [
Color(red: 0.4, green: 0.45, blue: 0.4).opacity(0.15),
.clear
] : [
Color(red: 0.3, green: 0.35, blue: 0.3).opacity(0.08),
.clear
],
center: .center,
startRadius: 0,
endRadius: 300
)
)
.scaleEffect(breathe ? 1.1 : 1.0)
.blur(radius: 80)
}
.ignoresSafeArea()
.onAppear {
withAnimation(.easeInOut(duration: 6).repeatForever(autoreverses: true)) {
breathe = true
}
}
.onDisappear {
breathe = false
}
}
}
struct ZenEnsoOrb: View {
@Environment(\.colorScheme) private var colorScheme
@State private var drawProgress: CGFloat = 0
@State private var breathe = false
var body: some View {
ZStack {
// Outer glow
Circle()
.stroke(
Color(red: 0.35, green: 0.4, blue: 0.35).opacity(colorScheme == .dark ? 0.3 : 0.15),
lineWidth: 4
)
.frame(width: 140, height: 140)
.blur(radius: 15)
.scaleEffect(breathe ? 1.1 : 0.95)
// Enso circle - incomplete for zen aesthetics
Circle()
.trim(from: 0, to: 0.85)
.stroke(
Color(red: 0.3, green: 0.35, blue: 0.3),
style: StrokeStyle(lineWidth: 5, lineCap: .round)
)
.frame(width: 100, height: 100)
.rotationEffect(.degrees(-90))
.scaleEffect(breathe ? 1.02 : 0.98)
}
.onAppear {
withAnimation(.easeInOut(duration: 5).repeatForever(autoreverses: true)) {
breathe = true
}
}
.onDisappear {
breathe = false
}
}
}
// MARK: - Neon Lock Screen Theme
struct NeonLockBackground: View {
@State private var pulse = false
var body: some View {
ZStack {
// Deep black base
Color(red: 0.02, green: 0.02, blue: 0.05)
// Grid lines
Canvas { context, size in
let spacing: CGFloat = 30
for y in stride(from: 0, to: size.height, by: spacing) {
var path = Path()
path.move(to: CGPoint(x: 0, y: y))
path.addLine(to: CGPoint(x: size.width, y: y))
context.stroke(path, with: .color(Color.cyan.opacity(0.08)), lineWidth: 0.5)
}
for x in stride(from: 0, to: size.width, by: spacing) {
var path = Path()
path.move(to: CGPoint(x: x, y: 0))
path.addLine(to: CGPoint(x: x, y: size.height))
context.stroke(path, with: .color(Color.cyan.opacity(0.08)), lineWidth: 0.5)
}
}
// Neon glow spots
Circle()
.fill(Color(red: 0, green: 1, blue: 0.82).opacity(pulse ? 0.3 : 0.15))
.frame(width: 300, height: 300)
.blur(radius: 80)
.offset(y: -100)
Circle()
.fill(Color(red: 1, green: 0, blue: 0.8).opacity(pulse ? 0.2 : 0.1))
.frame(width: 250, height: 250)
.blur(radius: 70)
.offset(x: 50, y: 150)
}
.ignoresSafeArea()
.onAppear {
withAnimation(.easeInOut(duration: 2).repeatForever(autoreverses: true)) {
pulse = true
}
}
.onDisappear {
pulse = false
}
}
}
struct NeonRingOrb: View {
@State private var rotate = false
@State private var pulse = false
var body: some View {
ZStack {
// Outer glow ring
Circle()
.stroke(
LinearGradient(
colors: [Color(red: 0, green: 1, blue: 0.82), Color(red: 1, green: 0, blue: 0.8)],
startPoint: .topLeading,
endPoint: .bottomTrailing
),
lineWidth: 4
)
.frame(width: 140, height: 140)
.blur(radius: 10)
.opacity(pulse ? 0.9 : 0.5)
.rotationEffect(.degrees(rotate ? 360 : 0))
// Inner ring
Circle()
.stroke(
LinearGradient(
colors: [Color(red: 0, green: 1, blue: 0.82), Color(red: 1, green: 0, blue: 0.8)],
startPoint: .topLeading,
endPoint: .bottomTrailing
),
lineWidth: 3
)
.frame(width: 100, height: 100)
.shadow(color: Color(red: 0, green: 1, blue: 0.82).opacity(0.6), radius: pulse ? 20 : 10)
// Center core
Circle()
.fill(Color.white)
.frame(width: 30, height: 30)
.shadow(color: .white.opacity(0.8), radius: 15)
}
.onAppear {
withAnimation(.linear(duration: 10).repeatForever(autoreverses: false)) {
rotate = true
}
withAnimation(.easeInOut(duration: 1.5).repeatForever(autoreverses: true)) {
pulse = true
}
}
.onDisappear {
rotate = false
pulse = false
}
}
}
// MARK: - Celestial Lock Screen Theme
struct CelestialLockBackground: View {
@Environment(\.colorScheme) private var colorScheme
@State private var twinkle = false
var body: some View {
ZStack {
// Deep space gradient
LinearGradient(
colors: colorScheme == .dark ? [
Color(red: 0.05, green: 0.05, blue: 0.12),
Color(red: 0.08, green: 0.06, blue: 0.15)
] : [
Color(red: 0.95, green: 0.94, blue: 0.98),
Color(red: 0.92, green: 0.9, blue: 0.96)
],
startPoint: .top,
endPoint: .bottom
)
// Star field (dark mode only)
if colorScheme == .dark {
Canvas { context, size in
for _ in 0..<50 {
let x = CGFloat.random(in: 0...size.width)
let y = CGFloat.random(in: 0...size.height)
let starSize = CGFloat.random(in: 1...3)
context.fill(
Path(ellipseIn: CGRect(x: x, y: y, width: starSize, height: starSize)),
with: .color(.white.opacity(Double.random(in: 0.3...0.8)))
)
}
}
.opacity(twinkle ? 0.8 : 1.0)
}
// Nebula glow
Circle()
.fill(Color(red: 1.0, green: 0.4, blue: 0.5).opacity(colorScheme == .dark ? 0.2 : 0.1))
.frame(width: 300, height: 300)
.blur(radius: 80)
.offset(y: -50)
Circle()
.fill(Color(red: 0.6, green: 0.4, blue: 0.9).opacity(colorScheme == .dark ? 0.15 : 0.08))
.frame(width: 250, height: 250)
.blur(radius: 60)
.offset(x: 80, y: 100)
}
.ignoresSafeArea()
.onAppear {
withAnimation(.easeInOut(duration: 3).repeatForever(autoreverses: true)) {
twinkle = true
}
}
.onDisappear {
twinkle = false
}
}
}
struct CelestialOrbsElement: View {
@Environment(\.colorScheme) private var colorScheme
@State private var float = false
@State private var rotate = false
private let orbColors: [Color] = [
Color(red: 1.0, green: 0.8, blue: 0.3), // Gold
Color(red: 1.0, green: 0.5, blue: 0.5), // Coral
Color(red: 0.6, green: 0.5, blue: 0.9) // Lavender
]
var body: some View {
ZStack {
// Orbit ring
Circle()
.stroke(Color.white.opacity(colorScheme == .dark ? 0.15 : 0.2), lineWidth: 1)
.frame(width: 140, height: 140)
.rotationEffect(.degrees(rotate ? 360 : 0))
// Orbiting orbs
ForEach(0..<3, id: \.self) { i in
Circle()
.fill(
RadialGradient(
colors: [orbColors[i], orbColors[i].opacity(0.6)],
center: .center,
startRadius: 0,
endRadius: 20
)
)
.frame(width: 28, height: 28)
.shadow(color: orbColors[i].opacity(0.6), radius: 10)
.offset(y: -70)
.rotationEffect(.degrees(Double(i) * 120 + (rotate ? 360 : 0)))
}
// Center star
Image(systemName: "sparkle")
.font(.system(size: 40, weight: .light))
.foregroundStyle(
LinearGradient(
colors: [Color(red: 1.0, green: 0.9, blue: 0.7), Color(red: 1.0, green: 0.7, blue: 0.6)],
startPoint: .top,
endPoint: .bottom
)
)
.shadow(color: Color(red: 1.0, green: 0.8, blue: 0.5).opacity(0.5), radius: 15)
.scaleEffect(float ? 1.1 : 0.95)
}
.onAppear {
withAnimation(.linear(duration: 20).repeatForever(autoreverses: false)) {
rotate = true
}
withAnimation(.easeInOut(duration: 3).repeatForever(autoreverses: true)) {
float = true
}
}
.onDisappear {
rotate = false
float = false
}
}
}
// MARK: - Editorial Lock Screen Theme
struct EditorialLockBackground: View {
@Environment(\.colorScheme) private var colorScheme
var body: some View {
ZStack {
// Solid elegant background
Color(colorScheme == .dark ? .black : .white)
// Subtle texture lines
VStack(spacing: 0) {
ForEach(0..<20, id: \.self) { _ in
Rectangle()
.fill(Color.primary.opacity(0.02))
.frame(height: 1)
Spacer()
}
}
.padding(.horizontal, 40)
}
.ignoresSafeArea()
}
}
struct EditorialFrameElement: View {
@Environment(\.colorScheme) private var colorScheme
@State private var appear = false
var body: some View {
ZStack {
// Elegant frame
RoundedRectangle(cornerRadius: 2)
.stroke(Color.primary.opacity(0.3), lineWidth: 1)
.frame(width: 120, height: 150)
// Inner accent line
VStack {
Rectangle()
.fill(Color.primary)
.frame(width: 40, height: 2)
Spacer()
Rectangle()
.fill(Color.primary)
.frame(width: 40, height: 2)
}
.frame(height: 130)
.opacity(appear ? 1 : 0)
// Center diamond
Image(systemName: "diamond")
.font(.system(size: 28, weight: .ultraLight))
.foregroundColor(.primary)
.opacity(appear ? 1 : 0.5)
.scaleEffect(appear ? 1 : 0.9)
}
.onAppear {
withAnimation(.easeOut(duration: 1)) {
appear = true
}
}
}
}
// MARK: - Mixtape Lock Screen Theme
struct MixtapeLockBackground: View {
@State private var shift = false
var body: some View {
ZStack {
// Warm retro gradient
LinearGradient(
colors: [
Color(red: 0.95, green: 0.45, blue: 0.35),
Color(red: 0.95, green: 0.65, blue: 0.25)
],
startPoint: shift ? .topLeading : .topTrailing,
endPoint: shift ? .bottomTrailing : .bottomLeading
)
}
.ignoresSafeArea()
.onAppear {
withAnimation(.easeInOut(duration: 5).repeatForever(autoreverses: true)) {
shift = true
}
}
.onDisappear {
shift = false
}
}
}
struct CassetteElement: View {
@State private var spin = false
var body: some View {
ZStack {
// Cassette body
RoundedRectangle(cornerRadius: 8)
.fill(Color.black.opacity(0.85))
.frame(width: 140, height: 90)
.shadow(color: .black.opacity(0.3), radius: 10)
// Label area
RoundedRectangle(cornerRadius: 4)
.fill(Color.white.opacity(0.9))
.frame(width: 100, height: 30)
.offset(y: -15)
// Reels
HStack(spacing: 40) {
Circle()
.fill(Color.white.opacity(0.8))
.frame(width: 30, height: 30)
.overlay(
Circle()
.fill(Color.black.opacity(0.6))
.frame(width: 10, height: 10)
)
.rotationEffect(.degrees(spin ? 360 : 0))
Circle()
.fill(Color.white.opacity(0.8))
.frame(width: 30, height: 30)
.overlay(
Circle()
.fill(Color.black.opacity(0.6))
.frame(width: 10, height: 10)
)
.rotationEffect(.degrees(spin ? 360 : 0))
}
.offset(y: 18)
}
.onAppear {
withAnimation(.linear(duration: 4).repeatForever(autoreverses: false)) {
spin = true
}
}
.onDisappear {
spin = false
}
}
}
// MARK: - Bloom Lock Screen Theme
struct BloomLockBackground: View {
@Environment(\.colorScheme) private var colorScheme
@State private var bloom = false
var body: some View {
ZStack {
// Garden gradient
LinearGradient(
colors: colorScheme == .dark ? [
Color(red: 0.05, green: 0.12, blue: 0.08),
Color(red: 0.08, green: 0.18, blue: 0.1)
] : [
Color(red: 0.95, green: 0.98, blue: 0.95),
Color(red: 0.9, green: 0.96, blue: 0.92)
],
startPoint: .top,
endPoint: .bottom
)
// Soft glows
Circle()
.fill(Color(red: 0.3, green: 0.7, blue: 0.4).opacity(colorScheme == .dark ? 0.2 : 0.1))
.frame(width: 300, height: 300)
.blur(radius: 80)
.offset(y: bloom ? -20 : 20)
Circle()
.fill(Color(red: 1.0, green: 0.6, blue: 0.7).opacity(colorScheme == .dark ? 0.15 : 0.08))
.frame(width: 200, height: 200)
.blur(radius: 60)
.offset(x: -50, y: bloom ? 80 : 120)
}
.ignoresSafeArea()
.onAppear {
withAnimation(.easeInOut(duration: 6).repeatForever(autoreverses: true)) {
bloom = true
}
}
.onDisappear {
bloom = false
}
}
}
struct FlowerElement: View {
@Environment(\.colorScheme) private var colorScheme
@State private var bloom = false
var body: some View {
ZStack {
// Petals
ForEach(0..<6, id: \.self) { i in
Ellipse()
.fill(
LinearGradient(
colors: [
Color(red: 1.0, green: 0.6, blue: 0.7),
Color(red: 1.0, green: 0.5, blue: 0.6)
],
startPoint: .top,
endPoint: .bottom
)
)
.frame(width: 30, height: bloom ? 60 : 45)
.offset(y: bloom ? -45 : -35)
.rotationEffect(.degrees(Double(i) * 60))
.shadow(color: Color(red: 1.0, green: 0.5, blue: 0.6).opacity(0.3), radius: 8)
}
// Center
Circle()
.fill(
RadialGradient(
colors: [
Color(red: 1.0, green: 0.9, blue: 0.6),
Color(red: 1.0, green: 0.85, blue: 0.4)
],
center: .center,
startRadius: 0,
endRadius: 25
)
)
.frame(width: 40, height: 40)
.shadow(color: Color(red: 1.0, green: 0.9, blue: 0.6).opacity(0.5), radius: 10)
}
.onAppear {
withAnimation(.easeInOut(duration: 4).repeatForever(autoreverses: true)) {
bloom = true
}
}
.onDisappear {
bloom = false
}
}
}
// MARK: - Heartfelt Lock Screen Theme
struct HeartfeltLockBackground: View {
@Environment(\.colorScheme) private var colorScheme
@State private var pulse = false
var body: some View {
ZStack {
// Soft pink gradient
LinearGradient(
colors: colorScheme == .dark ? [
Color(red: 0.15, green: 0.08, blue: 0.1),
Color(red: 0.1, green: 0.05, blue: 0.08)
] : [
Color(red: 1.0, green: 0.95, blue: 0.96),
Color(red: 0.98, green: 0.92, blue: 0.94)
],
startPoint: .top,
endPoint: .bottom
)
// Heart glow
Circle()
.fill(Color(red: 0.9, green: 0.45, blue: 0.55).opacity(colorScheme == .dark ? 0.2 : 0.1))
.frame(width: 300, height: 300)
.blur(radius: 80)
.scaleEffect(pulse ? 1.1 : 0.95)
}
.ignoresSafeArea()
.onAppear {
withAnimation(.easeInOut(duration: 1.5).repeatForever(autoreverses: true)) {
pulse = true
}
}
.onDisappear {
pulse = false
}
}
}
struct HeartElement: View {
@State private var beat = false
var body: some View {
ZStack {
// Glow
Image(systemName: "heart.fill")
.font(.system(size: 80))
.foregroundColor(Color(red: 0.9, green: 0.45, blue: 0.55))
.blur(radius: 20)
.scaleEffect(beat ? 1.15 : 0.9)
// Main heart
Image(systemName: "heart.fill")
.font(.system(size: 70))
.foregroundStyle(
LinearGradient(
colors: [
Color(red: 0.95, green: 0.55, blue: 0.6),
Color(red: 0.85, green: 0.35, blue: 0.45)
],
startPoint: .top,
endPoint: .bottom
)
)
.shadow(color: Color(red: 0.9, green: 0.45, blue: 0.55).opacity(0.4), radius: 15)
.scaleEffect(beat ? 1.08 : 0.95)
}
.onAppear {
withAnimation(.easeInOut(duration: 0.8).repeatForever(autoreverses: true)) {
beat = true
}
}
.onDisappear {
beat = false
}
}
}
// MARK: - Minimal Lock Screen Theme
struct MinimalLockBackground: View {
@Environment(\.colorScheme) private var colorScheme
var body: some View {
ZStack {
// Clean gradient
LinearGradient(
colors: colorScheme == .dark ? [
Color(red: 0.1, green: 0.1, blue: 0.1),
Color(red: 0.08, green: 0.08, blue: 0.08)
] : [
Color(red: 0.98, green: 0.96, blue: 0.94),
Color(red: 0.95, green: 0.93, blue: 0.9)
],
startPoint: .top,
endPoint: .bottom
)
// Subtle warm accent
Circle()
.fill(Color(red: 0.95, green: 0.6, blue: 0.5).opacity(colorScheme == .dark ? 0.08 : 0.05))
.frame(width: 400, height: 400)
.blur(radius: 100)
}
.ignoresSafeArea()
}
}
struct MinimalCircleElement: View {
@Environment(\.colorScheme) private var colorScheme
@State private var breathe = false
var body: some View {
ZStack {
// Outer ring
Circle()
.stroke(Color.primary.opacity(0.15), lineWidth: 1)
.frame(width: breathe ? 130 : 120, height: breathe ? 130 : 120)
// Middle ring
Circle()
.stroke(Color.primary.opacity(0.25), lineWidth: 1)
.frame(width: breathe ? 90 : 85, height: breathe ? 90 : 85)
// Inner circle
Circle()
.fill(Color(red: 0.95, green: 0.6, blue: 0.5).opacity(colorScheme == .dark ? 0.6 : 0.4))
.frame(width: 50, height: 50)
.scaleEffect(breathe ? 1.05 : 0.95)
}
.onAppear {
withAnimation(.easeInOut(duration: 4).repeatForever(autoreverses: true)) {
breathe = true
}
}
.onDisappear {
breathe = false
}
}
}
// MARK: - Luxe Lock Screen Theme
struct LuxeLockBackground: View {
@State private var shimmer = false
var body: some View {
ZStack {
// Rich dark background
LinearGradient(
colors: [
Color(red: 0.1, green: 0.08, blue: 0.06),
Color(red: 0.06, green: 0.04, blue: 0.02)
],
startPoint: .top,
endPoint: .bottom
)
// Gold shimmer
LinearGradient(
colors: [
.clear,
Color(red: 0.85, green: 0.7, blue: 0.45).opacity(0.1),
.clear
],
startPoint: shimmer ? .topLeading : .bottomTrailing,
endPoint: shimmer ? .bottomTrailing : .topLeading
)
}
.ignoresSafeArea()
.onAppear {
withAnimation(.easeInOut(duration: 4).repeatForever(autoreverses: true)) {
shimmer = true
}
}
.onDisappear {
shimmer = false
}
}
}
struct DiamondElement: View {
@State private var rotate = false
@State private var shimmer = false
var body: some View {
ZStack {
// Glow
Image(systemName: "diamond.fill")
.font(.system(size: 70))
.foregroundColor(Color(red: 0.85, green: 0.7, blue: 0.45))
.blur(radius: 25)
.opacity(shimmer ? 0.6 : 0.3)
// Diamond
Image(systemName: "diamond.fill")
.font(.system(size: 60))
.foregroundStyle(
LinearGradient(
colors: [
Color(red: 0.95, green: 0.85, blue: 0.6),
Color(red: 0.75, green: 0.6, blue: 0.35),
Color(red: 0.55, green: 0.45, blue: 0.25)
],
startPoint: shimmer ? .topLeading : .bottomTrailing,
endPoint: shimmer ? .bottomTrailing : .topLeading
)
)
.shadow(color: Color(red: 0.85, green: 0.7, blue: 0.45).opacity(0.5), radius: 20)
.rotationEffect(.degrees(rotate ? 5 : -5))
}
.onAppear {
withAnimation(.easeInOut(duration: 3).repeatForever(autoreverses: true)) {
rotate = true
}
withAnimation(.easeInOut(duration: 2).repeatForever(autoreverses: true)) {
shimmer = true
}
}
.onDisappear {
rotate = false
shimmer = false
}
}
}
// MARK: - Forecast Lock Screen Theme
struct ForecastLockBackground: View {
@State private var drift = false
var body: some View {
ZStack {
// Sky gradient
LinearGradient(
colors: [
Color(red: 0.55, green: 0.75, blue: 0.95),
Color(red: 0.4, green: 0.6, blue: 0.85),
Color(red: 0.3, green: 0.5, blue: 0.75)
],
startPoint: .top,
endPoint: .bottom
)
// Floating clouds
ForEach(0..<4, id: \.self) { i in
Image(systemName: "cloud.fill")
.font(.system(size: CGFloat.random(in: 40...80)))
.foregroundColor(.white.opacity(Double.random(in: 0.3...0.6)))
.offset(
x: CGFloat(i * 100 - 150) + (drift ? 20 : -20),
y: CGFloat(i * 80 - 200)
)
.blur(radius: CGFloat.random(in: 2...5))
}
}
.ignoresSafeArea()
.onAppear {
withAnimation(.easeInOut(duration: 8).repeatForever(autoreverses: true)) {
drift = true
}
}
.onDisappear {
drift = false
}
}
}
struct WeatherElement: View {
@State private var shine = false
var body: some View {
ZStack {
// Sun rays
ForEach(0..<8, id: \.self) { i in
Rectangle()
.fill(Color(red: 1.0, green: 0.9, blue: 0.5))
.frame(width: 3, height: 30)
.offset(y: -60)
.rotationEffect(.degrees(Double(i) * 45))
.opacity(shine ? 0.8 : 0.4)
}
// Sun glow
Circle()
.fill(Color(red: 1.0, green: 0.9, blue: 0.5).opacity(0.4))
.frame(width: 100, height: 100)
.blur(radius: 25)
// Sun
Circle()
.fill(
RadialGradient(
colors: [
Color(red: 1.0, green: 0.95, blue: 0.7),
Color(red: 1.0, green: 0.85, blue: 0.4)
],
center: .center,
startRadius: 0,
endRadius: 35
)
)
.frame(width: 70, height: 70)
.shadow(color: Color(red: 1.0, green: 0.85, blue: 0.4).opacity(0.6), radius: 20)
// Cloud accent
Image(systemName: "cloud.fill")
.font(.system(size: 35))
.foregroundColor(.white)
.offset(x: 40, y: 25)
.shadow(color: .black.opacity(0.1), radius: 5)
}
.onAppear {
withAnimation(.easeInOut(duration: 2).repeatForever(autoreverses: true)) {
shine = true
}
}
.onDisappear {
shine = false
}
}
}
// MARK: - Playful Lock Screen Theme
struct PlayfulLockBackground: View {
@Environment(\.colorScheme) private var colorScheme
var body: some View {
ZStack {
// Warm cream gradient
LinearGradient(
colors: colorScheme == .dark ? [
Color(red: 0.15, green: 0.12, blue: 0.1),
Color(red: 0.1, green: 0.08, blue: 0.06)
] : [
Color(red: 1.0, green: 0.98, blue: 0.95),
Color(red: 0.98, green: 0.96, blue: 0.92)
],
startPoint: .top,
endPoint: .bottom
)
// Colorful accents
Circle()
.fill(Color(red: 0.95, green: 0.55, blue: 0.35).opacity(colorScheme == .dark ? 0.15 : 0.08))
.frame(width: 200, height: 200)
.blur(radius: 60)
.offset(x: -80, y: -150)
Circle()
.fill(Color(red: 0.95, green: 0.75, blue: 0.35).opacity(colorScheme == .dark ? 0.12 : 0.06))
.frame(width: 180, height: 180)
.blur(radius: 50)
.offset(x: 100, y: 150)
}
.ignoresSafeArea()
}
}
struct PlayfulEmojiElement: View {
@State private var bounce = false
@State private var wiggle = false
var body: some View {
ZStack {
// Background circle
Circle()
.fill(Color(red: 0.95, green: 0.55, blue: 0.35).opacity(0.15))
.frame(width: 140, height: 140)
.scaleEffect(bounce ? 1.05 : 0.98)
// Main emoji
Text("😊")
.font(.system(size: 80))
.rotationEffect(.degrees(wiggle ? 8 : -8))
.scaleEffect(bounce ? 1.1 : 0.95)
}
.onAppear {
withAnimation(.easeInOut(duration: 0.6).repeatForever(autoreverses: true)) {
wiggle = true
}
withAnimation(.easeInOut(duration: 1.2).repeatForever(autoreverses: true)) {
bounce = true
}
}
.onDisappear {
wiggle = false
bounce = false
}
}
}
// MARK: - Journal Lock Screen Theme
struct JournalLockBackground: View {
@Environment(\.colorScheme) private var colorScheme
var body: some View {
ZStack {
// Warm paper background
Color(colorScheme == .dark ?
Color(red: 0.12, green: 0.1, blue: 0.08) :
Color(red: 0.95, green: 0.92, blue: 0.88)
)
// Paper texture lines
VStack(spacing: 28) {
ForEach(0..<25, id: \.self) { _ in
Rectangle()
.fill(Color.primary.opacity(colorScheme == .dark ? 0.08 : 0.06))
.frame(height: 1)
}
}
.padding(.horizontal, 50)
// Margin line
Rectangle()
.fill(Color(red: 0.85, green: 0.55, blue: 0.55).opacity(colorScheme == .dark ? 0.3 : 0.2))
.frame(width: 1)
.offset(x: -120)
}
.ignoresSafeArea()
}
}
struct JournalBookElement: View {
@Environment(\.colorScheme) private var colorScheme
@State private var open = false
var body: some View {
ZStack {
// Book shadow
RoundedRectangle(cornerRadius: 8)
.fill(Color.black.opacity(0.2))
.frame(width: 110, height: 140)
.offset(x: 5, y: 5)
.blur(radius: 8)
// Book cover
RoundedRectangle(cornerRadius: 8)
.fill(
LinearGradient(
colors: [
Color(red: 0.55, green: 0.45, blue: 0.35),
Color(red: 0.45, green: 0.35, blue: 0.28)
],
startPoint: .top,
endPoint: .bottom
)
)
.frame(width: 100, height: 130)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(Color(red: 0.65, green: 0.55, blue: 0.45), lineWidth: 2)
)
// Spine
Rectangle()
.fill(Color(red: 0.4, green: 0.32, blue: 0.25))
.frame(width: 8, height: 130)
.offset(x: -46)
// Title area
VStack(spacing: 8) {
Rectangle()
.fill(Color(red: 0.95, green: 0.9, blue: 0.82))
.frame(width: 60, height: 30)
.cornerRadius(2)
Image(systemName: "heart.fill")
.font(.system(size: 20))
.foregroundColor(Color(red: 0.85, green: 0.55, blue: 0.55))
}
.scaleEffect(open ? 1.02 : 1.0)
}
.onAppear {
withAnimation(.easeInOut(duration: 2).repeatForever(autoreverses: true)) {
open = true
}
}
.onDisappear {
open = false
}
}
}
// MARK: - Glassmorphic Button
struct GlassButton: View {
@Environment(\.colorScheme) private var colorScheme
let icon: String
let title: String
let action: () -> Void
@State private var isPressed = false
@State private var pulse = false
private var isDark: Bool { colorScheme == .dark }
private var foregroundColor: Color { isDark ? .white : .primary }
private var accentOpacity: Double { isDark ? 0.15 : 0.1 }
var body: some View {
Button(action: action) {
HStack(spacing: 14) {
ZStack {
// Pulse ring
Circle()
.stroke(lineWidth: 2)
.foregroundColor(foregroundColor.opacity(0.3))
.frame(width: 44, height: 44)
.scaleEffect(pulse ? 1.3 : 1)
.opacity(pulse ? 0 : 0.6)
// Icon background
Circle()
.fill(foregroundColor.opacity(accentOpacity))
.frame(width: 44, height: 44)
Image(systemName: icon)
.font(.system(size: 20, weight: .medium))
.foregroundColor(foregroundColor)
}
Text(title)
.font(.system(size: 17, weight: .semibold, design: .rounded))
.foregroundColor(foregroundColor)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 18)
.padding(.horizontal, 24)
.background(
ZStack {
// Glass effect
RoundedRectangle(cornerRadius: 20)
.fill(.ultraThinMaterial)
// Border gradient
RoundedRectangle(cornerRadius: 20)
.stroke(
LinearGradient(
colors: [
foregroundColor.opacity(0.3),
foregroundColor.opacity(0.1),
foregroundColor.opacity(0.15)
],
startPoint: .topLeading,
endPoint: .bottomTrailing
),
lineWidth: 1
)
// Inner highlight
RoundedRectangle(cornerRadius: 20)
.fill(
LinearGradient(
colors: [foregroundColor.opacity(0.08), .clear],
startPoint: .top,
endPoint: .bottom
)
)
}
)
.scaleEffect(isPressed ? 0.97 : 1)
}
.buttonStyle(PlainButtonStyle())
.simultaneousGesture(
DragGesture(minimumDistance: 0)
.onChanged { _ in
withAnimation(.easeInOut(duration: 0.1)) {
isPressed = true
}
}
.onEnded { _ in
withAnimation(.easeInOut(duration: 0.1)) {
isPressed = false
}
}
)
.onAppear {
withAnimation(
.easeInOut(duration: 2)
.repeatForever(autoreverses: false)
) {
pulse = true
}
}
}
}
// MARK: - Main Lock Screen View
struct LockScreenView: View {
@Environment(\.colorScheme) private var colorScheme
@ObservedObject var authManager: BiometricAuthManager
@State private var showError = false
@State private var showContent = false
// Read from AppStorage to match current theme, with optional override for previews
@AppStorage(UserDefaultsStore.Keys.lockScreenStyle.rawValue, store: GroupUserDefaults.groupDefaults)
private var lockScreenStyleRaw: Int = 0
var style: LockScreenStyle?
private var lockScreenStyle: LockScreenStyle {
if let override = style {
return override
}
return LockScreenStyle(rawValue: lockScreenStyleRaw) ?? .aurora
}
private var isDark: Bool { colorScheme == .dark }
// Style-dependent properties
private var primaryTextColor: Color {
switch lockScreenStyle {
case .neon:
return Color(red: 0, green: 1, blue: 0.82)
case .editorial:
return isDark ? .white : .black
case .mixtape:
return .white
case .luxe:
return Color(red: 0.95, green: 0.85, blue: 0.6)
case .forecast:
return .white
default:
return isDark ? .white : .primary
}
}
private var secondaryTextColor: Color {
switch lockScreenStyle {
case .neon:
return Color(red: 1, green: 0, blue: 0.8)
case .editorial:
return isDark ? .white.opacity(0.7) : .black.opacity(0.7)
case .mixtape:
return .white.opacity(0.9)
case .luxe:
return Color(red: 0.75, green: 0.6, blue: 0.35)
case .forecast:
return .white.opacity(0.9)
default:
return isDark ? .white.opacity(0.7) : .secondary
}
}
private var tertiaryTextColor: Color {
switch lockScreenStyle {
case .neon:
return Color.white.opacity(0.6)
case .editorial:
return isDark ? .white.opacity(0.5) : .black.opacity(0.5)
case .mixtape:
return .white.opacity(0.7)
case .luxe:
return Color(red: 0.65, green: 0.55, blue: 0.4).opacity(0.8)
case .forecast:
return .white.opacity(0.7)
default:
return isDark ? .white.opacity(0.5) : .secondary.opacity(0.8)
}
}
private var titleFont: Font {
switch lockScreenStyle {
case .neon:
return .system(size: 28, weight: .black, design: .monospaced)
case .editorial:
return .system(size: 30, weight: .ultraLight, design: .serif)
case .mixtape:
return .system(size: 28, weight: .black, design: .rounded)
case .zen:
return .system(size: 30, weight: .thin, design: .serif)
case .luxe:
return .system(size: 28, weight: .light, design: .serif)
case .playful:
return .system(size: 28, weight: .bold, design: .rounded)
case .journal:
return .system(size: 26, weight: .medium, design: .serif)
default:
return .system(size: 32, weight: .light, design: .serif)
}
}
private var titleText: String {
switch lockScreenStyle {
case .neon: return "UNLOCK YOUR"
case .editorial: return "Your Story"
case .mixtape: return "PRESS PLAY"
case .zen: return "Find Your"
case .heartfelt: return "Feel With"
case .luxe: return "Your Sanctuary"
case .forecast: return "Your Forecast"
case .playful: return "Hey There!"
case .journal: return "Your Journal"
case .bloom: return "Time to"
case .celestial: return "Your Feelings"
case .minimal: return "Simply"
default: return "Your Feelings"
}
}
private var subtitleText: String {
switch lockScreenStyle {
case .neon: return "FULL SIGNAL"
case .editorial: return "Awaits"
case .mixtape: return "ON YOUR MOODS"
case .zen: return "Inner Peace"
case .heartfelt: return "All Your Heart"
case .luxe: return "Awaits"
case .forecast: return "Is Ready"
case .playful: return "Let's Check In!"
case .journal: return "Is Private"
case .bloom: return "Bloom"
case .celestial: return "are safe here"
case .minimal: return "Know Yourself"
default: return "are safe here"
}
}
private var taglineText: String {
switch lockScreenStyle {
case .neon: return "Authenticate to sync your vibes"
case .editorial: return "Authenticate to continue"
case .mixtape: return "Authenticate to spin your tracks"
case .zen: return "Authenticate to begin your practice"
case .heartfelt: return "Authenticate to open your heart"
case .luxe: return "Authenticate for exclusive access"
case .forecast: return "Authenticate to check the weather"
case .playful: return "Authenticate to start the fun!"
case .journal: return "Authenticate to continue writing"
case .bloom: return "Authenticate to tend your garden"
case .celestial: return "Authenticate to explore the cosmos"
case .minimal: return "Authenticate to continue"
default: return "Authenticate to continue your journey"
}
}
var body: some View {
ZStack {
// Themed background
backgroundView
.accessibilityHidden(true)
// Floating particles (Aurora only)
if lockScreenStyle == .aurora {
FloatingParticlesView()
.accessibilityHidden(true)
}
// Main content
VStack(spacing: 0) {
Spacer()
// Central element
centralElement
.opacity(showContent ? 1 : 0)
.scaleEffect(showContent ? 1 : 0.8)
.accessibilityHidden(true)
Spacer()
.frame(height: 50)
// Text content
VStack(spacing: 12) {
Text(titleText)
.font(titleFont)
.foregroundColor(primaryTextColor)
.tracking(lockScreenStyle == .neon || lockScreenStyle == .mixtape ? 2 : 0)
Text(subtitleText)
.font(lockScreenStyle == .neon ? .system(size: 24, weight: .bold, design: .monospaced) : titleFont)
.foregroundColor(secondaryTextColor)
.tracking(lockScreenStyle == .neon || lockScreenStyle == .mixtape ? 2 : 0)
}
.multilineTextAlignment(.center)
.opacity(showContent ? 1 : 0)
.offset(y: showContent ? 0 : 20)
.accessibilityElement(children: .combine)
Spacer()
.frame(height: 16)
Text(taglineText)
.font(.system(size: 14, weight: .regular, design: .rounded))
.foregroundColor(tertiaryTextColor)
.opacity(showContent ? 1 : 0)
Spacer()
// Unlock button
themedButton
.disabled(authManager.isAuthenticating)
.opacity(showContent ? 1 : 0)
.offset(y: showContent ? 0 : 30)
.padding(.horizontal, 32)
.accessibilityIdentifier(AccessibilityID.LockScreen.unlockButton)
.accessibilityLabel("Unlock")
.accessibilityHint("Double tap to authenticate with \(authManager.biometricName)")
// Passcode button
if authManager.canUseDevicePasscode {
Button {
Task {
let success = await authManager.authenticate()
if !success {
showError = true
}
}
} label: {
Text("Or use your device passcode")
.font(.system(size: 13, weight: .medium, design: .rounded))
.foregroundColor(passcodeButtonColor)
}
.disabled(authManager.isAuthenticating)
.padding(.top, 16)
.opacity(showContent ? 1 : 0)
.accessibilityLabel("Use device passcode")
.accessibilityHint("Double tap to authenticate with your device passcode")
}
Spacer()
.frame(height: 50)
}
.padding()
}
.alert("Authentication Failed", isPresented: $showError) {
Button("Try Again") {
Task {
await authManager.authenticate()
}
}
.accessibilityIdentifier(AccessibilityID.LockScreen.tryAgainButton)
Button("Cancel", role: .cancel) { }
.accessibilityIdentifier(AccessibilityID.LockScreen.cancelButton)
} message: {
Text("Unable to verify your identity. Please try again.")
}
.onAppear {
withAnimation(.easeOut(duration: 0.8).delay(0.2)) {
showContent = true
}
if !authManager.isUnlocked && !authManager.isAuthenticating {
Task {
try? await Task.sleep(for: .milliseconds(800))
await authManager.authenticate()
}
}
}
}
// MARK: - Themed Components
@ViewBuilder
private var backgroundView: some View {
switch lockScreenStyle {
case .aurora:
AuroraBackground()
case .zen:
ZenLockBackground()
case .neon:
NeonLockBackground()
case .celestial:
CelestialLockBackground()
case .editorial:
EditorialLockBackground()
case .mixtape:
MixtapeLockBackground()
case .bloom:
BloomLockBackground()
case .heartfelt:
HeartfeltLockBackground()
case .minimal:
MinimalLockBackground()
case .luxe:
LuxeLockBackground()
case .forecast:
ForecastLockBackground()
case .playful:
PlayfulLockBackground()
case .journal:
JournalLockBackground()
}
}
@ViewBuilder
private var centralElement: some View {
switch lockScreenStyle {
case .aurora:
BreathingOrb()
case .zen:
ZenEnsoOrb()
case .neon:
NeonRingOrb()
case .celestial:
CelestialOrbsElement()
case .editorial:
EditorialFrameElement()
case .mixtape:
CassetteElement()
case .bloom:
FlowerElement()
case .heartfelt:
HeartElement()
case .minimal:
MinimalCircleElement()
case .luxe:
DiamondElement()
case .forecast:
WeatherElement()
case .playful:
PlayfulEmojiElement()
case .journal:
JournalBookElement()
}
}
@ViewBuilder
private var themedButton: some View {
switch lockScreenStyle {
case .neon:
NeonUnlockButton(
icon: authManager.biometricIcon,
title: "Unlock with \(authManager.biometricName)"
) {
Task {
let success = await authManager.authenticate()
if !success { showError = true }
}
}
case .luxe:
LuxeUnlockButton(
icon: authManager.biometricIcon,
title: "Unlock with \(authManager.biometricName)"
) {
Task {
let success = await authManager.authenticate()
if !success { showError = true }
}
}
case .mixtape:
MixtapeUnlockButton(
icon: authManager.biometricIcon,
title: "Unlock with \(authManager.biometricName)"
) {
Task {
let success = await authManager.authenticate()
if !success { showError = true }
}
}
default:
GlassButton(
icon: authManager.biometricIcon,
title: "Unlock with \(authManager.biometricName)"
) {
Task {
let success = await authManager.authenticate()
if !success { showError = true }
}
}
}
}
private var passcodeButtonColor: Color {
switch lockScreenStyle {
case .neon:
return Color(red: 0, green: 1, blue: 0.82).opacity(0.7)
case .luxe:
return Color(red: 0.85, green: 0.7, blue: 0.45).opacity(0.7)
case .mixtape, .forecast:
return .white.opacity(0.6)
case .editorial:
return isDark ? .white.opacity(0.5) : .black.opacity(0.5)
default:
return isDark ? .white.opacity(0.5) : .accentColor
}
}
}
// MARK: - Themed Unlock Buttons
struct NeonUnlockButton: View {
let icon: String
let title: String
let action: () -> Void
@State private var pulse = false
var body: some View {
Button(action: action) {
HStack(spacing: 14) {
Image(systemName: icon)
.font(.system(size: 20, weight: .bold))
Text(title)
.font(.system(size: 15, weight: .bold, design: .monospaced))
}
.foregroundStyle(
LinearGradient(
colors: [Color(red: 0, green: 1, blue: 0.82), Color(red: 1, green: 0, blue: 0.8)],
startPoint: .leading,
endPoint: .trailing
)
)
.frame(maxWidth: .infinity)
.padding(.vertical, 18)
.padding(.horizontal, 24)
.background(
RoundedRectangle(cornerRadius: 12)
.fill(Color.black)
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(
LinearGradient(
colors: [Color(red: 0, green: 1, blue: 0.82), Color(red: 1, green: 0, blue: 0.8)],
startPoint: .leading,
endPoint: .trailing
),
lineWidth: 2
)
)
.shadow(color: Color(red: 0, green: 1, blue: 0.82).opacity(pulse ? 0.5 : 0.2), radius: pulse ? 15 : 8)
)
}
.buttonStyle(PlainButtonStyle())
.onAppear {
withAnimation(.easeInOut(duration: 1.5).repeatForever(autoreverses: true)) {
pulse = true
}
}
.onDisappear {
pulse = false
}
}
}
struct LuxeUnlockButton: View {
let icon: String
let title: String
let action: () -> Void
var body: some View {
Button(action: action) {
HStack(spacing: 14) {
Image(systemName: icon)
.font(.system(size: 20, weight: .medium))
Text(title)
.font(.system(size: 16, weight: .medium, design: .serif))
}
.foregroundColor(Color(red: 0.95, green: 0.9, blue: 0.75))
.frame(maxWidth: .infinity)
.padding(.vertical, 18)
.padding(.horizontal, 24)
.background(
RoundedRectangle(cornerRadius: 14)
.fill(
LinearGradient(
colors: [
Color(red: 0.55, green: 0.45, blue: 0.25),
Color(red: 0.4, green: 0.32, blue: 0.18)
],
startPoint: .top,
endPoint: .bottom
)
)
.overlay(
RoundedRectangle(cornerRadius: 14)
.stroke(
LinearGradient(
colors: [
Color(red: 0.85, green: 0.7, blue: 0.45),
Color(red: 0.65, green: 0.52, blue: 0.3)
],
startPoint: .topLeading,
endPoint: .bottomTrailing
),
lineWidth: 1
)
)
.shadow(color: Color(red: 0.85, green: 0.7, blue: 0.45).opacity(0.3), radius: 10)
)
}
.buttonStyle(PlainButtonStyle())
}
}
struct MixtapeUnlockButton: View {
let icon: String
let title: String
let action: () -> Void
var body: some View {
Button(action: action) {
HStack(spacing: 14) {
Image(systemName: icon)
.font(.system(size: 20, weight: .bold))
Text(title)
.font(.system(size: 15, weight: .bold, design: .rounded))
}
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.padding(.vertical, 18)
.padding(.horizontal, 24)
.background(
RoundedRectangle(cornerRadius: 14)
.fill(Color.black.opacity(0.7))
.overlay(
RoundedRectangle(cornerRadius: 14)
.stroke(Color.white.opacity(0.3), lineWidth: 1)
)
)
}
.buttonStyle(PlainButtonStyle())
}
}
// MARK: - Previews
#Preview("Aurora") {
LockScreenView(authManager: BiometricAuthManager(), style: .aurora)
}
#Preview("Zen") {
LockScreenView(authManager: BiometricAuthManager(), style: .zen)
}
#Preview("Neon") {
LockScreenView(authManager: BiometricAuthManager(), style: .neon)
}
#Preview("Celestial") {
LockScreenView(authManager: BiometricAuthManager(), style: .celestial)
}
#Preview("Editorial") {
LockScreenView(authManager: BiometricAuthManager(), style: .editorial)
}
#Preview("Mixtape") {
LockScreenView(authManager: BiometricAuthManager(), style: .mixtape)
}
#Preview("Bloom") {
LockScreenView(authManager: BiometricAuthManager(), style: .bloom)
}
#Preview("Heartfelt") {
LockScreenView(authManager: BiometricAuthManager(), style: .heartfelt)
}
#Preview("Minimal") {
LockScreenView(authManager: BiometricAuthManager(), style: .minimal)
}
#Preview("Luxe") {
LockScreenView(authManager: BiometricAuthManager(), style: .luxe)
}
#Preview("Forecast") {
LockScreenView(authManager: BiometricAuthManager(), style: .forecast)
}
#Preview("Playful") {
LockScreenView(authManager: BiometricAuthManager(), style: .playful)
}
#Preview("Journal") {
LockScreenView(authManager: BiometricAuthManager(), style: .journal)
}