Fetch Data from Coredata For iOS 14 Widget

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:

Sample Image

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



Leave a reply



Submit