Files
Reflect/ReflectWidget/WidgetProviders.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
12 KiB
Swift

//
// WidgetProviders.swift
// ReflectWidget
//
// Timeline providers for widget data
//
import WidgetKit
import SwiftUI
import Intents
// MARK: - Timeline Creator
struct TimeLineCreator {
@MainActor static func createViews(daysBack: Int) -> [WatchTimelineView] {
var timeLineView = [WatchTimelineView]()
let latestDayToShow = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: UserDefaultsStore.getOnboarding())
let dates = Array(0...daysBack).map({
Calendar.current.date(byAdding: .day, value: -$0, to: latestDayToShow)!
})
// Use WidgetDataProvider for isolated widget data access
let dataProvider = WidgetDataProvider.shared
for date in dates {
let dayStart = Calendar.current.startOfDay(for: date)
let dayEnd = Calendar.current.date(bySettingHour: 23, minute: 59, second: 59, of: dayStart)!
let moodTint: MoodTintable.Type = UserDefaultsStore.moodTintable()
let moodImages: MoodImagable.Type = UserDefaultsStore.moodMoodImagable()
if let todayEntry = dataProvider.getData(startDate: dayStart, endDate: dayEnd, includedDays: []).first {
timeLineView.append(WatchTimelineView(image: moodImages.icon(forMood: todayEntry.mood),
graphic: moodImages.icon(forMood: todayEntry.mood),
date: dayStart,
color: moodTint.color(forMood: todayEntry.mood),
secondaryColor: moodTint.secondary(forMood: todayEntry.mood),
mood: todayEntry.mood))
} else {
timeLineView.append(WatchTimelineView(image: moodImages.icon(forMood: .missing),
graphic: moodImages.icon(forMood: .missing),
date: dayStart,
color: moodTint.color(forMood: .missing),
secondaryColor: moodTint.secondary(forMood: .missing),
mood: .missing))
}
}
timeLineView = timeLineView.sorted(by: { $0.date > $1.date })
return timeLineView
}
/// Creates sample preview data for widget picker - shows what widget looks like with mood data
static func createSampleViews(count: Int) -> [WatchTimelineView] {
var timeLineView = [WatchTimelineView]()
let sampleMoods: [Mood] = [.great, .good, .average, .good, .great, .average, .bad, .good, .great, .good, .average]
let moodTint: MoodTintable.Type = UserDefaultsStore.moodTintable()
let moodImages: MoodImagable.Type = UserDefaultsStore.moodMoodImagable()
for i in 0..<count {
let date = Calendar.current.date(byAdding: .day, value: -i, to: Date())!
let dayStart = Calendar.current.startOfDay(for: date)
let mood = sampleMoods[i % sampleMoods.count]
timeLineView.append(WatchTimelineView(
image: moodImages.icon(forMood: mood),
graphic: moodImages.icon(forMood: mood),
date: dayStart,
color: moodTint.color(forMood: mood),
secondaryColor: moodTint.secondary(forMood: mood),
mood: mood
))
}
return timeLineView
}
}
// MARK: - Timeline Widget Provider
struct Provider: @preconcurrency IntentTimelineProvider {
typealias Entry = SimpleEntry
typealias Intent = ConfigurationIntent
let timeLineCreator = TimeLineCreator()
func placeholder(in context: Context) -> SimpleEntry {
return SimpleEntry(date: Date(),
configuration: ConfigurationIntent(),
timeLineViews: TimeLineCreator.createSampleViews(count: 10))
}
@MainActor func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) {
// Use sample data for widget picker preview, real data otherwise
let timeLineViews: [WatchTimelineView]
if context.isPreview {
timeLineViews = TimeLineCreator.createSampleViews(count: 10)
} else {
timeLineViews = Array(TimeLineCreator.createViews(daysBack: 11).prefix(10))
}
let (hasSubscription, hasVotedToday, promptText) = checkSubscriptionAndVoteStatus()
let entry = SimpleEntry(date: Date(),
configuration: ConfigurationIntent(),
timeLineViews: timeLineViews,
hasSubscription: hasSubscription,
hasVotedToday: hasVotedToday,
promptText: promptText)
completion(entry)
}
@MainActor func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
let (hasSubscription, hasVotedToday, promptText) = checkSubscriptionAndVoteStatus()
let entry = SimpleEntry(date: Calendar.current.date(byAdding: .second, value: 15, to: Date())!,
configuration: ConfigurationIntent(),
timeLineViews: nil,
hasSubscription: hasSubscription,
hasVotedToday: hasVotedToday,
promptText: promptText)
let midNightEntry = SimpleEntry(date: Calendar.current.date(bySettingHour: 23, minute: 59, second: 59, of: Date())!,
configuration: ConfigurationIntent(),
timeLineViews: nil,
hasSubscription: hasSubscription,
hasVotedToday: hasVotedToday,
promptText: promptText)
let date = Calendar.current.date(byAdding: .second, value: 10, to: Date())!
let timeline = Timeline(entries: [entry, midNightEntry], policy: .after(date))
completion(timeline)
}
@MainActor
private func checkSubscriptionAndVoteStatus() -> (hasSubscription: Bool, hasVotedToday: Bool, promptText: String) {
let hasSubscription = GroupUserDefaults.groupDefaults.bool(forKey: UserDefaultsStore.Keys.hasActiveSubscription.rawValue)
let votingDate = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: UserDefaultsStore.getOnboarding())
let dayStart = Calendar.current.startOfDay(for: votingDate)
let dayEnd = Calendar.current.date(bySettingHour: 23, minute: 59, second: 59, of: dayStart) ?? dayStart
// Use WidgetDataProvider for isolated widget data access
let todayEntry = WidgetDataProvider.shared.getData(startDate: dayStart, endDate: dayEnd, includedDays: []).first
let hasVotedToday = todayEntry != nil && todayEntry?.mood != Mood.missing && todayEntry?.mood != Mood.placeholder
let promptText = UserDefaultsStore.personalityPackable().randomPushNotificationStrings().body
return (hasSubscription, hasVotedToday, promptText)
}
}
// MARK: - Vote Widget Provider
struct VoteWidgetProvider: TimelineProvider {
func placeholder(in context: Context) -> VoteWidgetEntry {
// Show sample "already voted" state for widget picker preview
let sampleStats = MoodStats(totalEntries: 30, moodCounts: [.great: 10, .good: 12, .average: 5, .bad: 2, .horrible: 1])
let promptText = UserDefaultsStore.personalityPackable().randomPushNotificationStrings().body
return VoteWidgetEntry(date: Date(), votingDate: Date(), hasSubscription: true, hasVotedToday: true, todaysMood: .good, stats: sampleStats, promptText: promptText)
}
func getSnapshot(in context: Context, completion: @escaping (VoteWidgetEntry) -> Void) {
// Show sample data for widget picker preview
if context.isPreview {
let sampleStats = MoodStats(totalEntries: 30, moodCounts: [.great: 10, .good: 12, .average: 5, .bad: 2, .horrible: 1])
let promptText = UserDefaultsStore.personalityPackable().randomPushNotificationStrings().body
let entry = VoteWidgetEntry(date: Date(), votingDate: Date(), hasSubscription: true, hasVotedToday: true, todaysMood: .good, stats: sampleStats, promptText: promptText)
completion(entry)
return
}
Task { @MainActor in
let entry = createEntry()
completion(entry)
}
}
func getTimeline(in context: Context, completion: @escaping (Timeline<VoteWidgetEntry>) -> Void) {
Task { @MainActor in
let entry = createEntry()
// Calculate next refresh time
let nextRefresh = calculateNextRefreshDate()
let timeline = Timeline(entries: [entry], policy: .after(nextRefresh))
completion(timeline)
}
}
/// Calculate when the widget should next refresh
/// Refreshes at: rating time (to show voting view) and midnight (for new day)
private func calculateNextRefreshDate() -> Date {
let now = Date()
let calendar = Calendar.current
// Get the rating time from onboarding data
let onboardingData = UserDefaultsStore.getOnboarding()
let ratingTimeComponents = calendar.dateComponents([.hour, .minute], from: onboardingData.date)
// Create today's rating time
var todayRatingComponents = calendar.dateComponents([.year, .month, .day], from: now)
todayRatingComponents.hour = ratingTimeComponents.hour
todayRatingComponents.minute = ratingTimeComponents.minute
let todayRatingTime = calendar.date(from: todayRatingComponents) ?? now
// Tomorrow's midnight
let midnight = calendar.startOfDay(for: calendar.date(byAdding: .day, value: 1, to: now)!)
// If we haven't passed today's rating time, refresh at rating time
if now < todayRatingTime {
return todayRatingTime
}
// Otherwise refresh at midnight
return midnight
}
@MainActor
private func createEntry() -> VoteWidgetEntry {
let hasSubscription = GroupUserDefaults.groupDefaults.bool(forKey: UserDefaultsStore.Keys.hasActiveSubscription.rawValue)
// Use WidgetDataProvider for isolated read-only data access
let dataProvider = WidgetDataProvider.shared
let votingDate = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: UserDefaultsStore.getOnboarding())
let dayStart = Calendar.current.startOfDay(for: votingDate)
let dayEnd = Calendar.current.date(bySettingHour: 23, minute: 59, second: 59, of: dayStart)!
// Check if user has voted today
let todayEntry = dataProvider.getData(startDate: dayStart, endDate: dayEnd, includedDays: []).first
let hasVotedToday = todayEntry != nil && todayEntry?.mood != Mood.missing && todayEntry?.mood != Mood.placeholder
// Get today's mood if voted
let todaysMood: Mood? = hasVotedToday ? todayEntry?.mood : nil
// Get stats for display after voting
var stats: MoodStats? = nil
if hasVotedToday {
let allEntries = dataProvider.getData(
startDate: Date(timeIntervalSince1970: 0),
endDate: Date(),
includedDays: []
)
let validEntries = allEntries.filter { $0.mood != Mood.missing && $0.mood != Mood.placeholder }
let totalCount = validEntries.count
if totalCount > 0 {
var moodCounts: [Mood: Int] = [:]
for entry in validEntries {
moodCounts[entry.mood, default: 0] += 1
}
stats = MoodStats(totalEntries: totalCount, moodCounts: moodCounts)
}
}
// Get random prompt text for voting view
let promptText = UserDefaultsStore.personalityPackable().randomPushNotificationStrings().body
return VoteWidgetEntry(
date: Date(),
votingDate: votingDate,
hasSubscription: hasSubscription,
hasVotedToday: hasVotedToday,
todaysMood: todaysMood,
stats: stats,
promptText: promptText
)
}
}