Consolidate extension data providers and add side effects catch-up
- Create unified ExtensionDataProvider for Widget and Watch targets - Remove duplicate WatchDataProvider and WatchConnectivityManager from Watch App - Add side effects catch-up mechanism in MoodLogger for widget votes - Process pending side effects on app launch and midnight background task - Reduce ~450 lines of duplicated code across targets 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -86,159 +86,3 @@ protocol DataControlling: MoodDataReading, MoodDataWriting, MoodDataDeleting, Mo
|
||||
// MARK: - DataController Conformance
|
||||
|
||||
extension DataController: DataControlling {}
|
||||
|
||||
// MARK: - Widget Data Provider
|
||||
|
||||
/// Lightweight read-only data provider for widgets
|
||||
/// Uses UserDefaults-cached data when possible to avoid SwiftData overhead
|
||||
@MainActor
|
||||
final class WidgetDataProvider: MoodDataReading {
|
||||
|
||||
static let shared = WidgetDataProvider()
|
||||
|
||||
private var _container: ModelContainer?
|
||||
|
||||
private var container: ModelContainer {
|
||||
if let existing = _container {
|
||||
return existing
|
||||
}
|
||||
let newContainer = SharedModelContainer.createWithFallback(useCloudKit: true)
|
||||
_container = newContainer
|
||||
return newContainer
|
||||
}
|
||||
|
||||
private var modelContext: ModelContext {
|
||||
container.mainContext
|
||||
}
|
||||
|
||||
private init() {}
|
||||
|
||||
// MARK: - MoodDataReading
|
||||
|
||||
func getEntry(byDate date: Date) -> MoodEntryModel? {
|
||||
let startDate = Calendar.current.startOfDay(for: date)
|
||||
let endDate = Calendar.current.date(byAdding: .day, value: 1, to: startDate)!
|
||||
|
||||
var descriptor = FetchDescriptor<MoodEntryModel>(
|
||||
predicate: #Predicate { entry in
|
||||
entry.forDate >= startDate && entry.forDate <= endDate
|
||||
},
|
||||
sortBy: [SortDescriptor(\.forDate, order: .forward)]
|
||||
)
|
||||
descriptor.fetchLimit = 1
|
||||
|
||||
return try? modelContext.fetch(descriptor).first
|
||||
}
|
||||
|
||||
func getData(startDate: Date, endDate: Date, includedDays: [Int]) -> [MoodEntryModel] {
|
||||
let weekDays = includedDays.isEmpty ? [1, 2, 3, 4, 5, 6, 7] : includedDays
|
||||
|
||||
let descriptor = FetchDescriptor<MoodEntryModel>(
|
||||
predicate: #Predicate { entry in
|
||||
entry.forDate >= startDate &&
|
||||
entry.forDate <= endDate &&
|
||||
weekDays.contains(entry.weekDay)
|
||||
},
|
||||
sortBy: [SortDescriptor(\.forDate, order: .forward)]
|
||||
)
|
||||
|
||||
return (try? modelContext.fetch(descriptor)) ?? []
|
||||
}
|
||||
|
||||
func splitIntoYearMonth(includedDays: [Int]) -> [Int: [Int: [MoodEntryModel]]] {
|
||||
let data = getData(
|
||||
startDate: Date(timeIntervalSince1970: 0),
|
||||
endDate: Date(),
|
||||
includedDays: includedDays
|
||||
).sorted { $0.forDate < $1.forDate }
|
||||
|
||||
guard let earliest = data.first,
|
||||
let latest = data.last else { return [:] }
|
||||
|
||||
let calendar = Calendar.current
|
||||
let earliestYear = calendar.component(.year, from: earliest.forDate)
|
||||
let latestYear = calendar.component(.year, from: latest.forDate)
|
||||
|
||||
var result = [Int: [Int: [MoodEntryModel]]]()
|
||||
|
||||
for year in earliestYear...latestYear {
|
||||
var monthData = [Int: [MoodEntryModel]]()
|
||||
|
||||
for month in 1...12 {
|
||||
var components = DateComponents()
|
||||
components.year = year
|
||||
components.month = month
|
||||
components.day = 1
|
||||
|
||||
guard let startOfMonth = calendar.date(from: components) else { continue }
|
||||
|
||||
let items = getData(
|
||||
startDate: startOfMonth,
|
||||
endDate: startOfMonth.endOfMonth,
|
||||
includedDays: [1, 2, 3, 4, 5, 6, 7]
|
||||
)
|
||||
|
||||
if !items.isEmpty {
|
||||
monthData[month] = items
|
||||
}
|
||||
}
|
||||
|
||||
result[year] = monthData
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
var earliestEntry: MoodEntryModel? {
|
||||
var descriptor = FetchDescriptor<MoodEntryModel>(
|
||||
sortBy: [SortDescriptor(\.forDate, order: .forward)]
|
||||
)
|
||||
descriptor.fetchLimit = 1
|
||||
return try? modelContext.fetch(descriptor).first
|
||||
}
|
||||
|
||||
var latestEntry: MoodEntryModel? {
|
||||
var descriptor = FetchDescriptor<MoodEntryModel>(
|
||||
sortBy: [SortDescriptor(\.forDate, order: .reverse)]
|
||||
)
|
||||
descriptor.fetchLimit = 1
|
||||
return try? modelContext.fetch(descriptor).first
|
||||
}
|
||||
|
||||
// MARK: - Widget-Specific Helpers
|
||||
|
||||
/// Get today's mood entry
|
||||
func getTodayEntry() -> MoodEntryModel? {
|
||||
getEntry(byDate: Date())
|
||||
}
|
||||
|
||||
/// Get the current streak count
|
||||
func getCurrentStreak() -> Int {
|
||||
let entries = getData(
|
||||
startDate: Calendar.current.date(byAdding: .day, value: -365, to: Date())!,
|
||||
endDate: Date(),
|
||||
includedDays: [1, 2, 3, 4, 5, 6, 7]
|
||||
).sorted { $0.forDate > $1.forDate }
|
||||
|
||||
var streak = 0
|
||||
var currentDate = Calendar.current.startOfDay(for: Date())
|
||||
|
||||
for entry in entries {
|
||||
let entryDate = Calendar.current.startOfDay(for: entry.forDate)
|
||||
|
||||
if entryDate == currentDate && entry.mood != .missing && entry.mood != .placeholder {
|
||||
streak += 1
|
||||
currentDate = Calendar.current.date(byAdding: .day, value: -1, to: currentDate)!
|
||||
} else if entryDate < currentDate {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return streak
|
||||
}
|
||||
|
||||
/// Invalidate cached container (call when data might have changed)
|
||||
func invalidateCache() {
|
||||
_container = nil
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user