Fetch data from CoreData for iOS 14 widget
First you need to create an AppGroup which will be used to create a Core Data Persistent Container (here is a good explanation how to do it)
Then you need to create your own CoreData stack (an example can be found when you create a new empty project with CoreData enabled).
- Accessing Core Data Stack in MVVM application
Assuming you have already created your Core Data model (here called DataModel
), you now need to set the container url to your custom shared container location:
- Share data between main App and Widget in SwiftUI for iOS 14
let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: <your_app_group>)!
let storeURL = containerURL.appendingPathComponent("DataModel.sqlite")
let description = NSPersistentStoreDescription(url: storeURL)
let container = NSPersistentContainer(name: "DataModel")
container.persistentStoreDescriptions = [description]
container.loadPersistentStores { ... }
Now you can get the managedObjectContext
from your shared Persistent Container:
let moc = CoreDataStack.shared.managedObjectContext
and perform a fetch request with it (more information here)
let predicate = NSPredicate(format: "attribute1 == %@", "test")
let request = NSFetchRequest<SomeItem>(entityName: "SomeItem")
let result = try moc.fetch(request)
Apart from all the links above I recommend you also read this tutorial about Core Data:
- Core Data with SwiftUI Tutorial: Getting Started
Here is a GitHub repository with different Widget examples including the Core Data Widget.
How to reload fetch data from CoreData in Widget iOS 14 when host App closes
The code responsible for fetching data is only in init
:
struct Provider: TimelineProvider {
var moc = PersistenceController.shared.managedObjectContext
var timerEntity:TimerEntity?
init(context : NSManagedObjectContext) {
self.moc = context
let request = NSFetchRequest<TimerEntity>(entityName: "TimerEntity")
do{
let result = try moc.fetch(request)
timerEntity = result.first
}
catch let error as NSError{
print("Could not fetch.\(error.userInfo)")
}
}
...
}
It is run only when the Provider
is initialised. You need to execute this NSFetchRequest
in the getTimeline
function as well, so your timerEntity
is updated.
Also, if you execute WidgetCenter.shared.reloadAllTimelines()
every time scenePhase
== .inactive
it might be too often and your Widget might stop getting updated.
inactive
is called whenever your app is moved from the background to the foreground (in both directions).
Try using background
instead - this will be called only when your app is minimised or killed:
.onChange(of: scenePhase) { newScenePhase in
if newScenePhase == .background {
WidgetCenter.shared.reloadAllTimelines()
}
}
Note: I'd recommend adding some safeguards around WidgetCenter.shared.reloadAllTimelines()
to make sure to not refresh it too often.
Fetch CoreData in a Widget
As far as I did read in the internet @NSFetchRequest is not working in Widgets.
I created a CoreDataManager class for the Widget which will fetch the data from "getTimeline"
My Code is a little bit more complex as my Widget is configurable but maybe it helps you to understand how to get it work.
First the DataController which is shared between the main app and the widget.
class DataController: ObservableObject {
let container: NSPersistentCloudKitContainer
init(inMemory: Bool = false) {
container = NSPersistentCloudKitContainer(name: "Main", managedObjectModel: Self.model)
let storeURL = URL.storeURL(for: "MyGroup", databaseName: "Main")
let storeDescription = NSPersistentStoreDescription(url: storeURL)
storeDescription.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(
containerIdentifier: "Identifier"
)
container.persistentStoreDescriptions = [storeDescription]
guard let description = container.persistentStoreDescriptions.first else {
Log.shared.add(.coreData, .error, "###\(#function): Failed to retrieve a persistent store description.")
fatalError("###\(#function): Failed to retrieve a persistent store description.")
}
description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
if inMemory {
container.persistentStoreDescriptions.first?.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores { (_, error) in
if let error = error {
Log.shared.add(.cloudKit, .fault, "Fatal error loading store \(error)")
fatalError("Fatal error loading store \(error.localizedDescription)")
}
}
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
}
static let model: NSManagedObjectModel = {
guard let url = Bundle.main.url(forResource: "Main", withExtension: "momd") else {
fatalError("Failed to locate model file.")
}
guard let managedObjectModel = NSManagedObjectModel(contentsOf: url) else {
fatalError("Failed to load model file.")
}
return managedObjectModel
}()
}
CoreDataManager for the Widget
class CoreDataManager {
var vehicleArray = [Vehicle]()
let dataController: DataController
private var observers = [NSObjectProtocol]()
init(_ dataController: DataController) {
self.dataController = dataController
fetchVehicles()
/// Add Observer to observe CoreData changes and reload data
observers.append(
NotificationCenter.default.addObserver(forName: .NSPersistentStoreRemoteChange, object: nil, queue: .main) { _ in //swiftlint:disable:this line_length discarded_notification_center_observer
self.fetchVehicles()
}
)
}
deinit {
/// Remove Observer when CoreDataManager is de-initialized
observers.forEach(NotificationCenter.default.removeObserver)
}
/// Fetches all Vehicles from CoreData
func fetchVehicles() {
defer {
WidgetCenter.shared.reloadAllTimelines()
}
dataController.container.viewContext.refreshAllObjects()
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Vehicle")
do {
guard let vehicleArray = try dataController.container.viewContext.fetch(fetchRequest) as? [Vehicle] else {
return
}
self.vehicleArray = vehicleArray
} catch {
print("Failed to fetch: \(error)")
}
}
}
I did add my CoreData Model "Vehicle" to the WidgetEntry
struct WidgetEntry: TimelineEntry {
let date: Date
let configuration: SelectedVehicleIntent
var vehicle: Vehicle?
}
Timeline Provider is fetching all CoreData when the timeline is reloaded and injecting my Core data Model into the WidgetEntry
struct Provider: IntentTimelineProvider {
/// Access to CoreDataManger
let dataController = DataController()
let coreDataManager: CoreDataManager
init() {
coreDataManager = CoreDataManager(dataController)
}
/// Placeholder for Widget
func placeholder(in context: Context) -> WidgetEntry {
WidgetEntry(date: Date(), configuration: SelectedVehicleIntent(), vehicle: Vehicle.example)
}
/// Provides a timeline entry representing the current time and state of a widget.
func getSnapshot(for configuration: SelectedVehicleIntent, in context: Context, completion: @escaping (WidgetEntry) -> Void) { //swiftlint:disable:this line_length
vehicleForWidget(for: configuration) { selectedVehicle in
let entry = WidgetEntry(
date: Date(),
configuration: configuration,
vehicle: selectedVehicle
)
completion(entry)
}
}
/// Provides an array of timeline entries for the current time and, optionally, any future times to update a widget.
func getTimeline(for configuration: SelectedVehicleIntent, in context: Context, completion: @escaping (Timeline<WidgetEntry>) -> Void) { //swiftlint:disable:this line_length
coreDataManager.fetchVehicles()
/// Fetches the vehicle selected in the configuration
vehicleForWidget(for: configuration) { selectedVehicle in
let currentDate = Date()
var entries: [WidgetEntry] = []
// Create a date that's 60 minutes in the future.
let nextUpdateDate = Calendar.current.date(byAdding: .minute, value: 60, to: currentDate)!
// Generate an Entry
let entry = WidgetEntry(date: currentDate, configuration: configuration, vehicle: selectedVehicle)
entries.append(entry)
let timeline = Timeline(entries: entries, policy: .after(nextUpdateDate))
completion(timeline)
}
}
/// Fetches the Vehicle defined in the Configuration
/// - Parameters:
/// - configuration: Intent Configuration
/// - completion: completion handler returning the selected Vehicle
func vehicleForWidget(for configuration: SelectedVehicleIntent, completion: @escaping (Vehicle?) -> Void) {
var selectedVehicle: Vehicle?
defer {
completion(selectedVehicle)
}
for vehicle in coreDataManager.vehicleArray where vehicle.uuid?.uuidString == configuration.favoriteVehicle?.identifier { //swiftlint:disable:this line_length
selectedVehicle = vehicle
}
}
}
And finally the WidgetView
struct WidgetView: View {
var entry: Provider.Entry
var body: some View {
VStack {
Text("CoreData: \(entry.vehicle?.fuel?.count ?? 99999)")
Text("Name: \(entry.vehicle?.brand ?? "No Vehicle found")")
Divider()
Text("Refreshed: \(entry.date, style: .relative)")
.font(.caption)
}
.padding(.all)
}
}
How to use fetch data in Intent Handler for editing Widget iOS 14?
I could display a list in the widget using fetch data from CoreData
like the code below:
IntentHandler.swift
import WidgetKit
import SwiftUI
import CoreData
import Intents
class IntentHandler: INExtension, ConfigurationIntentHandling {
var moc = PersistenceController.shared.managedObjectContext
var timerEntity_0:TimerEntity?
var timerEntity_1:TimerEntity?
var timerEntity_2:TimerEntity?
func provideNameOptionsCollection(for intent: ConfigurationIntent, searchTerm: String?, with completion: @escaping (INObjectCollection<NSString>?, Error?) -> Void) {
let request = NSFetchRequest<TimerEntity>(entityName: "TimerEntity")
do{
let result = try moc.fetch(request)
timerEntity_0 = result[0]
timerEntity_1 = result[1]
timerEntity_2 = result[2]
}
catch let error as NSError{
print("Could not fetch.\(error.userInfo)")
}
let nameIdentifiers:[NSString] = [
NSString(string: timerEntity_0?.task ?? "default"),
NSString(string: timerEntity_1?.task ?? "default"),
NSString(string: timerEntity_2?.task ?? "default")
// "meeting",
// "cooking",
// "shoping"
]
let allNameIdentifiers = INObjectCollection(items: nameIdentifiers)
completion(allNameIdentifiers,nil)
}
override func handler(for intent: INIntent) -> Any {
return self
}
}
How to display data related to the same row in CoreData at IntentConfiguration Widget view iOS 14?
You can create a custom type for your configuration parameter. Currently you're using String
which limits you to one value only.
Instead create a custom type, let's call it Item
:
Now you have the identifier
and displayString
properties for your Item
which can be mapped to the UUID
and task
properties of your model.
Then, in the IntentHandler
instead of INObjectCollection<NSString>?
you need to provide INObjectCollection<Item>?
in the completion.
Assuming you already have your results
fetched from Core Data, you only need to map them to the Item
objects:
let results = try moc.fetch(request)
let items = results.map {
Item(identifier: $0.id.uuidString, display: $0.task)
}
completion(items, nil)
This way you can use the display
property to show readable information to the user but also have the identifier
property which can be later used to retrieve the Core Data model.
iOS14.5 Widget data not up-to-date
Update:
I added the following code to refresh the core data before I fetch. Everything work as expect.
// refresh the core data
try? viewContext.setQueryGenerationFrom(.current)
viewContext.refreshAllObjects()
Related Topics
How to Compare Two Dictionaries in Swift
How to Create Array of Unique Object List in Swift
How to Convert a Decimal Number to Binary in Swift
Nsattributedstring, Change the Font Overall But Keep All Other Attributes
Class Conforming to Protocol as Function Parameter in Swift
Choosing Coredata Entities from Form Picker
Swift: How to Use Preprocessor Flags (Like '#If Debug') to Implement API Keys
What Is _: in Swift Telling Me
Swift Protocol Inheritance and Protocol Conformance Issue
Using a Type Variable in a Generic
Swift Alamofire: How to Get the Http Response Status Code
Difference Between Computed Property and Property Set With Closure
Using Dateformatter on a Unix Timestamp