WidgetKit and CoreData/CloudKit
First, add a managedObject variable to the TimelineProvider and a initializer:
Code Block struct Provider: IntentTimelineProvider { var managedObjectContext : NSManagedObjectContext init(context : NSManagedObjectContext) { self.managedObjectContext = context } ...
Then initialize the persistentContainer within the Widget struct and pass the persistentContainer.viewContext into the new Provider initializer:
Code Block @main struct Widget_Extension: Widget { private let kind: String = "Widget_Extension" public var body: some WidgetConfiguration { IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider(context: persistentContainer.viewContext), placeholder: PlaceholderView()) { entry in Widget_ExtensionEntryView(entry: entry) } .configurationDisplayName("My Widget") .description("This is an example widget.") } var persistentContainer: NSPersistentCloudKitContainer = {... return container }()
The Provider now has access to your CoreData+CloudKit data.
Thanks for posting your solution to this online. I'm having the same problem, but it would be great it for you could help a little more. For example, all your code is working fine, but I can't seem to figure out how to use my core data entity in a Text() view in my widget.
Below is my code, and any help with this or more sample code would be great. Thanks 🙏
Code Block swift import WidgetKit import SwiftUI import Intents import CoreData struct Provider: IntentTimelineProvider { func placeholder(in context: Context) -> SimpleEntry { SimpleEntry(date: Date(), configuration: ConfigurationIntent()) } func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) { let entry = SimpleEntry(date: Date(), configuration: configuration) completion(entry) } func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) { var entries: [SimpleEntry] = [] // Generate a timeline consisting of five entries an hour apart, starting from the current date. let currentDate = Date() for hourOffset in 0 ..< 5 { let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)! let entry = SimpleEntry(date: entryDate, configuration: configuration) entries.append(entry) } let timeline = Timeline(entries: entries, policy: .atEnd) completion(timeline) } var managedObjectContext : NSManagedObjectContext init(context : NSManagedObjectContext) { self.managedObjectContext = context } } struct SimpleEntry: TimelineEntry { let date: Date let configuration: ConfigurationIntent } struct Test_WidgetEntryView : View { var entry: Provider.Entry @FetchRequest(entity: Order.entity(), sortDescriptors: [], predicate: NSPredicate(format: "status != %@", Status.completed.rawValue)) var orders: FetchedResults<Order> var body: some View { Text("This is where I would like to display the text from one of the entity elements") } } @main struct Test_Widget: Widget { let kind: String = "Test_Widget" public var body: some WidgetConfiguration { IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider(context: persistentContainer.viewContext)) { entry in Test_WidgetEntryView(entry: entry) } .configurationDisplayName("My Widget") .description("This is an example widget.") } // // MARK: - Core Data stack var persistentContainer: NSPersistentCloudKitContainer = { /* The persistent container for the application. This implementation creates and returns a container, having loaded the store for the application to it. This property is optional since there are legitimate error conditions that could cause the creation of the store to fail. */ let container = NSPersistentCloudKitContainer(name: "PostMaster") // let storeURL = URL.storeURL(for: "group.com.seannagle.ipostmaster", databaseName: "iPostMaster") // let storeDescription = NSPersistentStoreDescription(url: storeURL) container.loadPersistentStores(completionHandler: { (storeDescription, error) in if let error = error as NSError? { // Replace this implementation with code to handle the error appropriately. // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. /* Typical reasons for an error here include: * The parent directory does not exist, cannot be created, or disallows writing. * The persistent store is not accessible, due to permissions or data protection when the device is locked. * The device is out of space. * The store could not be migrated to the current model version. Check the error message to determine what the actual problem was. */ fatalError("Unresolved error \(error), \(error.userInfo)") } }) // container.persistentStoreDescriptions = [storeDescription] container.viewContext.automaticallyMergesChangesFromParent = true container.viewContext.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy return container }() }
I ran many tests with and without WidgetKit and I discovered that no matter what I tried, I could never get NSPersistentCloudKitContainer to sync while an app is in the background. There is no amount of time that you can wait either, the sync will simply not happen. I tried querying Core Data during BackgroundTasks and the data is simply never there. I can see that a sync operation gets queued with a priority of 2 but only when an app becomes active does the sync even run.
Widgets always run in the background.
Since NSPersistentCloudKitContainer only runs while an app is in the foreground and since widgets only run in the background, the sync will never happen unless you launch the app. This isn't ideal because say you have the app installed on another device like a watch, your widget on your phone will never receive the updates that you do on the watch.
At least, not with NSPersistentCloudKitContainer.
After contacting Apple, they confirmed that this is what is happening. To make matters worst, the sync operation is not publicly exposed therefore you can't manually trigger it.
Their solution is to manually sync your records using CloudKit and store that data into Core Data manually, which, imo, defeats the purpose of NSPersistentCloudKitContainer. Now truth be told, you don't really need the entire database data in your widget, so you can just take a snapshot of the data that you need, store that in CloudKit and retrieve it in your widget manually as needed.
I strongly recommend that anyone who needs NSPersistentCloudKitContainer to run in the background send Apple some feedback. If enough of us do, they might introduce this feature in the future.
I don’t think this last comment is accurate, at least as of iOS 15. In digital lounges at WWDC Apple engineers explained it is possible for NSPersistentCloudKitContainer to sync while your app is open in the background. It seems though the work is scheduled with utility priority. In my testing it will sync if you make 4 changes - once enough updates are accumulated it’ll process them. But even then my widget is showing the changes from the 3rd update because it hasn’t yet finished updating it seems, and attempting to delay it causes the code not to execute I assume because iOS suspends the app again very soon after.
So basically it may eventually sync in background, only if your app is already open in background, and it may not be reliable and your widget might not get the most up-to-date data. Maybe this can be improved by utilizing background task API, that’s what they suggested trying in the digital lounge.
Do note they also said NSPersistentCloudKitContainer does not support multi-process sync so only your app should be attempting to sync. And even if a widget were to attempt sync, it’ll never really be able to because iOS doesn’t give it enough time to execute, and widgets don’t run in the background they’re only running when they need to get more timeline entries for example, and widgets don’t get the app’s push notifications which is what enables background syncs to be scheduled. Your app will need to try to keep the widget up to date as opposed to the widget attempting to sync and keep itself up to date.
In testing today, syncing in the background with NSPersistentCloudKitContainer
seems to be working more reliably with the iOS 16 SDK. The first time you change something on another device it still seems to schedule a task to perform that work with utility priority (via -[NSCloudKitMirroringDelegate checkAndScheduleImportIfNecessary:andStartAfterDate:]
which logs Scheduling automated import with activity CKSchedulerActivity
priority 2 Utility), so it doesn't execute right away. If you change it again then I'm seeing it does immediately import the two accumulated changes (NSCloudKitMirroringDelegate
Beginning automated import - ImportActivity - in response to activity, then -[PFCloudKitImportRecordsWorkItem applyAccumulatedChangesToStore:inManagedObjectContext:withStoreMonitor:madeChanges:error:]
followed by NSCloudKitMirroringImportRequest
Importing updated records) which triggers NSManagedObjectContextObjectsDidChange,
NSPersistentStoreRemoteChange,
and NSPersistentCloudKitContainer.eventChangedNotification.
This process seems to repeat - the 3rd change will be scheduled, 4th will cause import.
In my app, I have logic to detect if the widget needs to reload from NSManagedObjectContextObjectsDidChange
examining the info in the notification's userInfo.
In this specific scenario updating one record, NSRefreshedObjectsKey
contains an instance of my NSManagedObject
subclass, so I call WidgetCenter.shared.reloadAllTimelines()
after DispatchQueue.main.async
and the database is updated at that time so the widget gets a new timeline that includes the latest change.
But do note that sync won't happen unless the app is open in the background as the remote notifications do not launch your app, so for example restarting the device will result in sync not occurring until they open the app again. Perhaps background task API can be explored to attempt to keep the widget up-to-date otherwise.
Hi, Cristosv
Hope you can help me understand whatI'm doing wrong.
I have shared the CoreData with widget and follow your instruction so I have this situation now
struct DictaWordWidget: Widget {
let kind: String = "DictaWordWidget"
var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "DictaWord")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
print(storeDescription)
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy
return container
}()
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider(context: persistentContainer.viewContext)) { entry in
WidgetView(entry: entry)
}
.supportedFamilies([.systemMedium, .systemLarge])
.configurationDisplayName("My Widget")
.description("This is an example widget.")
}
}
so after in the provider the situation is like this
struct Provider: TimelineProvider {
var managedObjectContext : NSManagedObjectContext
typealias Entry = SimpleEntry
init(context : NSManagedObjectContext) {
print("init provider")
self.managedObjectContext = context
}
and finally in the getTimeline
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
let request = NSFetchRequest<WordEntity>(entityName: "WordEntity")
do {
let words = try managedObjectContext.fetch(request)
let oneWord = Array(words.prefix(1))
let entry = SimpleEntry(date: .now, words: oneWord)
print("result: \(words)")ì
let timeline = Timeline(entries: [entry], policy: .after(.now.advanced(by: 60 * 60 * 30)))
completion(timeline)ì
} catch let error {
print("Error fetching coredata words: \(error.localizedDescription)")
}
}
my SimpleEntry
struct SimpleEntry: TimelineEntry {
let date: Date
let words: [WordEntity]
}
but when result is printed the array is empty and I really not understand why. All seams correct at code side no runtime error
thanks
You can always file a Technical Support Incident if you need specific code level help.
We have a sample from WWDC24 - Sharing Core Data Objects between iCloud users that demonstrates the integration of CloudKit and WidgetKit. Please feel free to ask follow up questions if needed.
Rico
WWDR - DTS - Software Engineer