Files
Reflect/ReflectWidget/ReflectLiveActivity.swift
Trey t 0442eab1f8 Rebrand entire project from Feels to Reflect
Complete rename across all bundle IDs, App Groups, CloudKit containers,
StoreKit product IDs, data store filenames, URL schemes, logger subsystems,
Swift identifiers, user-facing strings (7 languages), file names, directory
names, Xcode project, schemes, assets, and documentation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 11:47:16 -06:00

266 lines
9.4 KiB
Swift

//
// ReflectLiveActivity.swift
// ReflectWidget
//
// Live Activity for mood streak tracking (Dynamic Island + Lock Screen)
//
import WidgetKit
import SwiftUI
import ActivityKit
// MARK: - Live Activity Widget
// Note: MoodStreakAttributes is defined in MoodStreakActivity.swift (Shared folder)
struct MoodStreakLiveActivity: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: MoodStreakAttributes.self) { context in
// Lock Screen / StandBy view
MoodStreakLockScreenView(context: context)
} dynamicIsland: { context in
DynamicIsland {
// Expanded view
DynamicIslandExpandedRegion(.leading) {
HStack(spacing: 8) {
Image(systemName: "flame.fill")
.foregroundColor(.orange)
Text("\(context.state.currentStreak)")
.font(.title2.bold())
}
}
DynamicIslandExpandedRegion(.trailing) {
if context.state.hasLoggedToday {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)
.font(.title2)
} else {
Text("Log now")
.font(.caption)
.foregroundColor(.secondary)
}
}
DynamicIslandExpandedRegion(.center) {
Text(context.state.hasLoggedToday ? "Streak: \(context.state.currentStreak) days" : (context.state.currentStreak > 0 ? "Don't break your streak!" : "Start your streak!"))
.font(.headline)
}
DynamicIslandExpandedRegion(.bottom) {
if !context.state.hasLoggedToday {
Text("Voting closes at midnight")
.font(.caption)
.foregroundColor(.secondary)
} else {
HStack {
Circle()
.fill(Color(hex: context.state.lastMoodColor))
.frame(width: 20, height: 20)
Text("Today: \(context.state.lastMoodLogged)")
.font(.subheadline)
}
}
}
} compactLeading: {
Image(systemName: "flame.fill")
.foregroundColor(.orange)
} compactTrailing: {
Text("\(context.state.currentStreak)")
.font(.caption.bold())
} minimal: {
Image(systemName: "flame.fill")
.foregroundColor(.orange)
}
}
}
}
// MARK: - Lock Screen View
struct MoodStreakLockScreenView: View {
let context: ActivityViewContext<MoodStreakAttributes>
var body: some View {
HStack(spacing: 16) {
// Streak indicator
VStack(spacing: 4) {
Image(systemName: "flame.fill")
.font(.title)
.foregroundColor(.orange)
Text("\(context.state.currentStreak)")
.font(.title.bold())
Text("day streak")
.font(.caption)
.foregroundColor(.secondary)
}
Divider()
.frame(height: 50)
// Status
VStack(alignment: .leading, spacing: 8) {
if context.state.hasLoggedToday {
HStack(spacing: 8) {
Circle()
.fill(Color(hex: context.state.lastMoodColor))
.frame(width: 24, height: 24)
VStack(alignment: .leading) {
Text("Today's mood")
.font(.caption)
.foregroundColor(.secondary)
Text(context.state.lastMoodLogged)
.font(.headline)
}
}
} else {
VStack(alignment: .leading) {
Text(context.state.currentStreak > 0 ? "Don't break your streak!" : "Start your streak!")
.font(.headline)
Text("Tap to log your mood")
.font(.caption)
.foregroundColor(.secondary)
}
}
}
Spacer()
}
.padding()
.activityBackgroundTint(Color(.systemBackground).opacity(0.8))
}
}
// MARK: - Preview Sample Data
extension MoodStreakAttributes {
static var preview: MoodStreakAttributes {
MoodStreakAttributes(startDate: Date())
}
}
extension MoodStreakAttributes.ContentState {
static var notLogged: MoodStreakAttributes.ContentState {
MoodStreakAttributes.ContentState(
currentStreak: 7,
lastMoodLogged: "None",
lastMoodColor: "#888888",
hasLoggedToday: false,
votingWindowEnd: Calendar.current.date(bySettingHour: 23, minute: 59, second: 59, of: Date()) ?? Date()
)
}
static var loggedGreat: MoodStreakAttributes.ContentState {
MoodStreakAttributes.ContentState(
currentStreak: 15,
lastMoodLogged: "Great",
lastMoodColor: MoodTints.Default.color(forMood: .great).toHex() ?? "#4CAF50",
hasLoggedToday: true,
votingWindowEnd: Calendar.current.date(bySettingHour: 23, minute: 59, second: 59, of: Date()) ?? Date()
)
}
static var loggedGood: MoodStreakAttributes.ContentState {
MoodStreakAttributes.ContentState(
currentStreak: 30,
lastMoodLogged: "Good",
lastMoodColor: MoodTints.Default.color(forMood: .good).toHex() ?? "#8BC34A",
hasLoggedToday: true,
votingWindowEnd: Calendar.current.date(bySettingHour: 23, minute: 59, second: 59, of: Date()) ?? Date()
)
}
static var loggedAverage: MoodStreakAttributes.ContentState {
MoodStreakAttributes.ContentState(
currentStreak: 10,
lastMoodLogged: "Average",
lastMoodColor: MoodTints.Default.color(forMood: .average).toHex() ?? "#FFC107",
hasLoggedToday: true,
votingWindowEnd: Calendar.current.date(bySettingHour: 23, minute: 59, second: 59, of: Date()) ?? Date()
)
}
static var loggedBad: MoodStreakAttributes.ContentState {
MoodStreakAttributes.ContentState(
currentStreak: 5,
lastMoodLogged: "Bad",
lastMoodColor: MoodTints.Default.color(forMood: .bad).toHex() ?? "#FF9800",
hasLoggedToday: true,
votingWindowEnd: Calendar.current.date(bySettingHour: 23, minute: 59, second: 59, of: Date()) ?? Date()
)
}
static var loggedHorrible: MoodStreakAttributes.ContentState {
MoodStreakAttributes.ContentState(
currentStreak: 3,
lastMoodLogged: "Horrible",
lastMoodColor: MoodTints.Default.color(forMood: .horrible).toHex() ?? "#F44336",
hasLoggedToday: true,
votingWindowEnd: Calendar.current.date(bySettingHour: 23, minute: 59, second: 59, of: Date()) ?? Date()
)
}
}
// MARK: - Live Activity Previews
#Preview("Lock Screen - Not Logged", as: .content, using: MoodStreakAttributes.preview) {
MoodStreakLiveActivity()
} contentStates: {
MoodStreakAttributes.ContentState.notLogged
}
#Preview("Lock Screen - Great", as: .content, using: MoodStreakAttributes.preview) {
MoodStreakLiveActivity()
} contentStates: {
MoodStreakAttributes.ContentState.loggedGreat
}
#Preview("Lock Screen - Good", as: .content, using: MoodStreakAttributes.preview) {
MoodStreakLiveActivity()
} contentStates: {
MoodStreakAttributes.ContentState.loggedGood
}
#Preview("Lock Screen - Average", as: .content, using: MoodStreakAttributes.preview) {
MoodStreakLiveActivity()
} contentStates: {
MoodStreakAttributes.ContentState.loggedAverage
}
#Preview("Lock Screen - Bad", as: .content, using: MoodStreakAttributes.preview) {
MoodStreakLiveActivity()
} contentStates: {
MoodStreakAttributes.ContentState.loggedBad
}
#Preview("Lock Screen - Horrible", as: .content, using: MoodStreakAttributes.preview) {
MoodStreakLiveActivity()
} contentStates: {
MoodStreakAttributes.ContentState.loggedHorrible
}
// MARK: - Dynamic Island Previews
#Preview("Dynamic Island Expanded - Not Logged", as: .dynamicIsland(.expanded), using: MoodStreakAttributes.preview) {
MoodStreakLiveActivity()
} contentStates: {
MoodStreakAttributes.ContentState.notLogged
}
#Preview("Dynamic Island Expanded - Logged", as: .dynamicIsland(.expanded), using: MoodStreakAttributes.preview) {
MoodStreakLiveActivity()
} contentStates: {
MoodStreakAttributes.ContentState.loggedGreat
}
#Preview("Dynamic Island Compact", as: .dynamicIsland(.compact), using: MoodStreakAttributes.preview) {
MoodStreakLiveActivity()
} contentStates: {
MoodStreakAttributes.ContentState.loggedGreat
}
#Preview("Dynamic Island Minimal", as: .dynamicIsland(.minimal), using: MoodStreakAttributes.preview) {
MoodStreakLiveActivity()
} contentStates: {
MoodStreakAttributes.ContentState.loggedGreat
}