I have a SwiftUI app. It fetches records through CoreData. And I want to show some records on a widget. I understand that I need to use AppGroup to share data between an app and its associated widget.
import Foundation
import CoreData
import CloudKit
class DataManager {
static let instance = DataManager()
let container: NSPersistentContainer
let context: NSManagedObjectContext
init() {
container = NSPersistentCloudKitContainer(name: "DataMama")
container.persistentStoreDescriptions = [NSPersistentStoreDescription(url: FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: group identifier)!.appendingPathComponent("Trash.sqlite"))]
container.loadPersistentStores(completionHandler: { (description, error) in
if let error = error as NSError? {
print("Unresolved error \(error), \(error.userInfo)")
}
})
context = container.viewContext
context.automaticallyMergesChangesFromParent = true
context.mergePolicy = NSMergePolicy(merge: .mergeByPropertyObjectTrumpMergePolicyType)
}
func save() {
do {
try container.viewContext.save()
print("Saved successfully")
} catch {
print("Error in saving data: \(error.localizedDescription)")
}
}
}
// ViewModel //
import Foundation
import CoreData
import WidgetKit
class ViewModel: ObservableObject {
let manager = DataManager()
@Published var records: [Little] = []
init() {
fetchRecords()
}
func fetchRecords() {
let request = NSFetchRequest<Little>(entityName: "Little")
do {
records = try manager.context.fetch(request)
records.sort { lhs, rhs in
lhs.trashDate! < rhs.trashDate!
}
} catch {
print("Fetch error for DataManager: \(error.localizedDescription)")
}
WidgetCenter.shared.reloadAllTimelines()
}
}
So I have a view model that fetches data for the app as shown above. Now, my question is how should my widget get data from CoreData? Should the widget get data from CoreData through DataManager? I have read some questions here and also read some articles around the world. This article ( https://dev.classmethod.jp/articles/widget-coredate-introduction/ ) suggests that you let the Widget struct access CoreData through DataManager. If that's a correct fashion, how should the getTimeline function in the TimelineProvider struct get data? This question also suggests the same. Thank you for your reading my question.
A solution is not to rely on a Data Manager class and its associates but to use UserDefaults so that Widget's getTimeline method will be called when ContentView submits data to it. By this approach, you can update Widget's View.
import SwiftUI
import WidgetKit
struct ContentView: View {
var body: some View {
VStack {
Button {
let task0 = TaskItem(id: UUID(), name: "Dish washing", isComplete: false, dueDate: Date.now.addingTimeInterval(86400))
let task1 = TaskItem(id: UUID(), name: "Bed making", isComplete: false, dueDate: Date.now.addingTimeInterval(86400 * 2))
saveTasksToWidget(tasks: [task0, task1])
} label: {
Image(systemName: "globe")
.imageScale(.large)
}
.tint(.pink)
.buttonStyle(.borderedProminent)
}
.padding()
}
private func saveTasksToWidget(tasks: [TaskItem]) {
guard let sharedDefaults = UserDefaults(suiteName: "group.abc") else {
print("Failed to get shared UserDefaults.")
return
}
do {
let encoder = JSONEncoder()
let encodedData = try encoder.encode(tasks)
sharedDefaults.set(encodedData, forKey: "widgetTasksArray")
WidgetCenter.shared.reloadAllTimelines()
} catch {
print("Error encoding tasks: \(error)")
}
}
}
// TaskItem.swift //
import Foundation
struct TaskItem: Codable, Identifiable {
let id: UUID
let name: String
let isComplete: Bool
let dueDate: Date
static let previewTasks = [
TaskItem(id: UUID(), name: "Rose", isComplete: false, dueDate: Date.now.addingTimeInterval(86400)),
TaskItem(id: UUID(), name: "Chrisanthumum", isComplete: true, dueDate: Date.now.addingTimeInterval(86400 * 2)),
TaskItem(id: UUID(), name: "Garden Dahlia", isComplete: false, dueDate: Date.now.addingTimeInterval(86400 * 3))
]
}
// CrazyWidget.swift //
import WidgetKit
import SwiftUI
struct SimpleEntry: TimelineEntry {
let date: Date
let tasks: [TaskItem]
}
struct Provider: TimelineProvider {
let appGroupID = "group.abc"
let dataKey = "widgetTasksArray"
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(), tasks: TaskItem.previewTasks)
}
func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
let currentTasks = loadTasks()
let entry = SimpleEntry(date: Date(), tasks: currentTasks)
completion(entry)
}
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
let currentTasks = loadTasks()
let entry = SimpleEntry(date: Date(), tasks: currentTasks)
let nextUpdate = Calendar.current.date(byAdding: .minute, value: 1, to: Date())!
let timeline = Timeline(entries: [entry], policy: .after(nextUpdate))
completion(timeline)
}
private func loadTasks() -> [TaskItem] {
var taskItems: [TaskItem] = []
if let sharedDefaults = UserDefaults(suiteName: appGroupID), let savedData = sharedDefaults.data(forKey: dataKey) {
do {
let decoder = JSONDecoder()
taskItems = try decoder.decode([TaskItem].self, from: savedData)
} catch {
print("Error decoding tasks from App Group: \(error)")
taskItems = TaskItem.previewTasks // Fallback on decoding failure
}
} else {
taskItems = TaskItem.previewTasks // Fallback on access failure
}
return taskItems
}
}
struct CrazyWidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
VStack(alignment: .leading) {
Text("Pending Tasks")
.font(.headline)
.foregroundColor(.blue)
Divider()
if entry.tasks.isEmpty {
Text("All clear! No tasks found.")
.font(.caption)
.foregroundColor(.secondary)
} else {
VStack(alignment: .leading, spacing: 4) {
ForEach(entry.tasks.prefix(3)) { task in
HStack {
Image(systemName: task.isComplete ? "checkmark.circle.fill" : "circle")
.foregroundColor(task.isComplete ? .green : .orange)
Text(task.name)
.font(.caption)
.strikethrough(task.isComplete)
.lineLimit(1)
}
}
}
}
}
.padding()
}
}
struct CrazyWidget: Widget {
let kind: String = "RailMe"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
CrazyWidgetEntryView(entry: entry)
}
.configurationDisplayName("RailMe GGGGG")
.description("View your current tasks saved from the main app.")
.supportedFamilies([.systemSmall, .systemMedium])
}
}
#Preview("Small - Full Data") {
CrazyWidgetEntryView(entry: SimpleEntry(date: Date(), tasks: TaskItem.previewTasks))
}