Thanks for being a part of WWDC25!

How did we do? We’d love to know your thoughts on this year’s conference. Take the survey here

WidgetKit with Data from CoreData

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 couple of articles that I have read suggest that I get CoreData records from my DataManager like the following. It's kind of redundant as it accesses DataManager inside the TimelineProvider struct and also inside the Widget struct. That's why I wonder if this is the right approach.


import WidgetKit
import SwiftUI
import CoreData

struct Provider: TimelineProvider {
    func placeholder(in context: Context) -> SimpleEntry {
        ...
    }

    func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
        ...
    }

    func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        let manager = CloudDataManager()
        var records: [Little] = []
        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 CloudDataManager: \(error.localizedDescription)")
        }
        
        var items: [Item] = []
        for record in records {
            let item = Item(trashDate: record.trashDate ?? Date.now, imageSelection: Int(record.imageSelection))
            items.append(item)
        }
        
        let entry = Timeline(entries: [SimpleEntry(date: Date(), items: items)], policy: .atEnd)
        completion(entry)
    }
}

struct SimpleEntry: TimelineEntry {
    let date: Date
    let items: [Item]
}

struct LittleGuyEntryView : View {
    var entry: Provider.Entry

    var body: some View {
        ZStack {
            ContainerRelativeShape()
                .fill(.widgetBackground)
            VStack {
                if entry.items.count > 0 {
                    ForEach(entry.items, id: \.id) { item in
                        ...
                    }
                }
            }
            .padding()
        }
    }
}

struct LittleGuy: Widget {
    let manager = DataManager()
    let kind: String = "App name"

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: Provider()) { entry in
            LittleTrashEntryView(entry: entry)
                .environment(\.managedObjectContext, manager.context)
                .containerBackground(.fill.tertiary, for: .widget)
        }
        .contentMarginsDisabled()
        .supportedFamilies([.systemSmall])
    }
}

As widgets and the main iOS app are on the same device, they can use the same CoreData stack.

I have a CoreData class with methods to access the stack in the Widget target. It's instantiated in the WigetExtensionBundle as a global let so it's available everywhere.

Whenever I need to get data from CoreData I can just call something like coredata.shared.getMyData().

In my timeline method (not getTimeline as I'm using AppIntentTimelineProvider) I just call coredata.shared.getMyData().

Your DataManager class is my CoreData class, and in my case I'm creating one global instance of it, while you're doing this everywhere: let manager = DataManager().

With a global you can access it anywhere. The timeline() method just grabs my data with something like coredata.getMyData(), or in your case manager.fetchRecords().

You seem to be writing your fetchRecords() method to get your data inside the getTimeline() method, i.e.:

let request = NSFetchRequest<Little>(entityName: "Little")
        do {
            records = try manager.context.fetch(request)
...

That should be put into your DataManager() class.

Also, use a predicate to sort your data in the call to CoreData, rather than sorting the records once you have them:

records.sort { lhs, rhs in ... // <-- remove this

Thanks for the tip, darkpaw. But I'm afraid you misunderstand my question. It's underlined. Do I have to let the widget pull data directly from CoreData data manager (DataManager)? In other words, is there any way I could use my existing view model (ViewModel) to deliver data to the widget?

You seem to be writing your fetchRecords() method to get your data inside the getTimeline() method, i.e.:

That should be put into your DataManager() class.

Okay. Thanks. I have done all dirty work through my view model (ViewModel) including the data sorting. That's why I want to know if I need to do it with the data manager, again, just for the widget. That's what I mean by 'redundant' or superfluous.

Well, I'm terribly sorry if I misunderstood your question, but it's quite difficult to make sense of what your question actually was given you posted:

  1. A DataManager class that just creates a connection to Core Data.
  2. A ViewModel class with a fetchRecords method that uses an instance of your DataManager class.
  3. Your TimelineProvider showing the same code from your fetchRecords method inside your getTimeline method. (As in, why the duplication?)

If your question was the underlined question, "how should my widget get data from CoreData?" then I already answered it by explaing how I do it in my working widgets.

You can use any method you want to get data into your widget, whether you pull it directly from the DataManager (via Core Data) or your ViewModel (which has already pulled it from Core Data).

Have you tried both methods? Did one or both of them work? Which one has the simpler code? Go with that one.

Remember, widgets shouldn't be considered actively running code. You tell the timeline provider what data the widget should use and what it should look like at a snapshot in time. The widget isn't sitting there reading your data from Core Data, so how you want to provide the data to it isn't 100% relevant. You only have to be performant in the getSnapshot method because that's used in transient situations like the preview gallery.

Since you haven't responded, other than a comment, and you haven't accepted anything as an answer, here's a little more clarification in case you're still misunderstanding how to do this:

  1. Move the fetchRecords method into the DataManager class.
  2. Use a predicate to sort the data rather than sorting it after you've retrieved the records. It's faster.
  3. Change your ViewModel to something like this:
class ViewModel: ObservableObject {
	let manager = DataManager()
	@Published var records: [Little] = []

	init() {
		records = manager.fetchRecords()
		WidgetCenter.shared.reloadAllTimelines()
	}
}
  1. DataManager can be shared between both your iOS target (where ViewModel resides) and your widget target, so change the target membership so it's shared.
  2. In your widget target, your getTimeline method would be something like this:
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
	let manager = DataManager()
	var items: [Item] = []
	for record in manager.fetchRecords() {
		let item = Item(trashDate: record.trashDate ?? Date.now, imageSelection: Int(record.imageSelection))
		items.append(item)
	}
	
	let entry = Timeline(entries: [SimpleEntry(date: Date(), items: items)], policy: .atEnd)
	completion(entry)
}

That should show you how to get data from Core Data into your widget.

And, just to answer your other question, no you can't use your ViewModel as it isn't in the widget target, and you cannot call reloadTimelines() from the widget.

If you're still having issues, then I'm sorry, but I've given you all the help I can. You haven't said whether you've attempted anything I've given you.

WidgetKit with Data from CoreData
 
 
Q