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>
This commit is contained in:
265
ReflectWidget/ReflectLiveActivity.swift
Normal file
265
ReflectWidget/ReflectLiveActivity.swift
Normal file
@@ -0,0 +1,265 @@
|
||||
//
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user