- CloudKit sync is now always enabled for all users - Remove useCloudKit setting and toggle from Settings - Remove CloudKitSyncMonitor usage (package can be removed) - Remove container switching logic since sync is always on - Update SharedModelContainer defaults to enable CloudKit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
245 lines
7.3 KiB
Swift
245 lines
7.3 KiB
Swift
//
|
|
// DataControllerProtocol.swift
|
|
// Feels
|
|
//
|
|
// Protocol defining the data access interface for mood entries.
|
|
// Enables dependency injection and testability.
|
|
//
|
|
|
|
import Foundation
|
|
import SwiftData
|
|
|
|
// MARK: - Data Controller Protocol
|
|
|
|
/// Protocol defining read-only data access for mood entries
|
|
/// Used by widgets and other components that only need to read data
|
|
@MainActor
|
|
protocol MoodDataReading {
|
|
/// Get a single entry for a specific date
|
|
func getEntry(byDate date: Date) -> MoodEntryModel?
|
|
|
|
/// Get entries within a date range, filtered by weekdays
|
|
func getData(startDate: Date, endDate: Date, includedDays: [Int]) -> [MoodEntryModel]
|
|
|
|
/// Get all data organized by year and month
|
|
func splitIntoYearMonth(includedDays: [Int]) -> [Int: [Int: [MoodEntryModel]]]
|
|
|
|
/// The earliest entry in the database
|
|
var earliestEntry: MoodEntryModel? { get }
|
|
|
|
/// The latest entry in the database
|
|
var latestEntry: MoodEntryModel? { get }
|
|
}
|
|
|
|
/// Protocol defining write operations for mood entries
|
|
@MainActor
|
|
protocol MoodDataWriting {
|
|
/// Add a new mood entry
|
|
func add(mood: Mood, forDate date: Date, entryType: EntryType)
|
|
|
|
/// Update the mood for an existing entry
|
|
@discardableResult
|
|
func update(entryDate: Date, withMood mood: Mood) -> Bool
|
|
|
|
/// Update notes for an entry
|
|
@discardableResult
|
|
func updateNotes(forDate date: Date, notes: String?) -> Bool
|
|
|
|
/// Update photo for an entry
|
|
@discardableResult
|
|
func updatePhoto(forDate date: Date, photoID: UUID?) -> Bool
|
|
|
|
/// Fill in missing dates with placeholder entries
|
|
func fillInMissingDates()
|
|
|
|
/// Fix incorrect weekday values
|
|
func fixWrongWeekdays()
|
|
}
|
|
|
|
/// Protocol for data deletion operations
|
|
@MainActor
|
|
protocol MoodDataDeleting {
|
|
/// Clear all entries from the database
|
|
func clearDB()
|
|
|
|
/// Delete entries from the last N days
|
|
func deleteLast(numberOfEntries: Int)
|
|
}
|
|
|
|
/// Protocol for persistence operations
|
|
@MainActor
|
|
protocol MoodDataPersisting {
|
|
/// Save pending changes
|
|
func save()
|
|
|
|
/// Save and notify listeners
|
|
func saveAndRunDataListeners()
|
|
|
|
/// Add a listener for data changes
|
|
func addNewDataListener(closure: @escaping (() -> Void))
|
|
}
|
|
|
|
/// Combined protocol for full data controller functionality
|
|
@MainActor
|
|
protocol DataControlling: MoodDataReading, MoodDataWriting, MoodDataDeleting, MoodDataPersisting {}
|
|
|
|
// 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
|
|
}
|
|
}
|