WidgetKit and CoreData/CloudKit

Any insights on how to incorporate CloudKit (or CoreData) in the WidgetKit extension? Where in the WidgetKit api do I make the asynchronous call to load the data to be available for the TimelineProvider?
Post not yet marked as solved Up vote post of cristosv Down vote post of cristosv
6.9k views

Replies

Hi cristosv, you can make an asynchronous call in both the snapshot and timeline methods of TimelineProvider. Just remember to call the completion block when you're done. There's some more discussion of this in Widgets Code-along, part 3, and an example in the related sample code.
Post not yet marked as solved Up vote reply of izzy Down vote reply of izzy
I might have answered my own question.

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.
  • Hi cristosv can you please review my reply under here maybe you can help me

    thanks

Add a Comment
This worked for me. Thank you so much cristosv. Wish I saw this 4 hours ago.
Hi cristosv,

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
    }()
}


This worked for me as well. Except, it only works the initial time. When core data gets updated, my app receives the update, fetches from core data again except it always returns stale data. Has anyone had this working at 100% yet?
Hi there. So this is the official and best way of doing this? Is this working all the time? @tkaravou stated 3 weeks ago that his calls return stale data and it's not working 100% of the time. Maybe there were some ios 14 beta bugs (i'm thinking maybe he was still running on beta verion, idk)? Really looking forward to an answer. Thank you. :D
Short Answer: You can't achieve Core Data / iCloud sync with NSPersistentCloudKitContainer in Widgets

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

  • One part ofit nrigoni is you have to enable icloud for the extension. Unfortunately, it then fails for me because the bundle ids are different.

Add a Comment

You can always file a Technical Support Incident if you need specific code level help.