How do I refresh my SwiftUI view when my core data store has been updated from a background extension?

3.4k views Asked by At

I have a SwiftUI app with an intents extension target. I also have a core data store located on a shared app group.

The intents extension can successfully write data to the store using newBackgroundContext() in the Shortcuts app. My SwiftUI app can read the data from the app group in viewContext but only when I force quit the app and re-open it.

I think I need a way to let my app know that it has been updated in a different context but I'm not sure how do do that?

I've tried setting context.automaticallyMergesChangesFromParent = true in the app delegate and adding context.refreshAllObjects() to onAppear in the SwiftUI view but that doesn't seem to make any difference.

mergeChanges(fromContextDidSave:) seems promising but I'm not sure how to get the Notification.

I'm new to Core Data and very confused, so any pointers in the right direction would be great, thanks!

Here's a sanitised version of my current code:

App Delegate

 lazy var persistentContainer: NSCustomPersistentContainer = {
        let container = NSCustomPersistentContainer(name: "exampleContainerName")
        container.loadPersistentStores { description, error in
            if let error = error {
                print("error loading persistent core data store")
            }
        }
        return container
    }()

    func saveContext () {
        let context = persistentContainer.viewContext
        if context.hasChanges {
            do {
                try context.save()
            } catch {
                print("error saving core data context")
            }
        }
    }

class NSCustomPersistentContainer: NSPersistentContainer {
    override open class func defaultDirectoryURL() -> URL {
        var storeURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "exampleAppGroupName")
        storeURL = storeURL?.appendingPathComponent("exampleContainerName")
        return storeURL!
    }
}

Scene Delegate

guard let context = (UIApplication.shared.delegate as? AppDelegate)?.persistentContainer.viewContext else {
            fatalError("Unable to read managed object context.")
}

if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
            window.rootViewController = UIHostingController(rootView: ExampleView.environment(\.managedObjectContext, context))
            self.window = window
            window.makeKeyAndVisible()
}

Swift UI View

import SwiftUI
import CoreData

struct ExampleView: View {

    @Environment(\.managedObjectContext) var managedObjectContext
    @FetchRequest(
        entity: ExampleEntityName.entity(),
        sortDescriptors: [
            NSSortDescriptor(keyPath: \ExampleEntityName.date, ascending: false),
        ]
    ) var items: FetchedResults<ExampleEntityName>

    var body: some View {
        VStack {
            List(items, id: \.self) { item in
                Text("\(item.name)")
            }
        }
    }
}

Intent Extension

class NSCustomPersistentContainer: NSPersistentContainer {
    override open class func defaultDirectoryURL() -> URL {
        var storeURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "exampleContainerName")
        storeURL = storeURL?.appendingPathComponent("exampleAppGroupName")
        return storeURL!
     }
}

let persistentContainer: NSCustomPersistentContainer = {
    let container = NSCustomPersistentContainer(name: "exampleContainerName")
    container.loadPersistentStores { description, error in
        if let error = error {
            print("error loading persistent core data store: \(error)")
        }
    }
    return container
}()

let context = persistentContainer.newBackgroundContext()

let entity = NSEntityDescription.entity(forEntityName: "ExampleEntityName", in: context)
let newEntry = NSManagedObject(entity: entity!, insertInto: context)
newEntry.setValue(Date(), forKey: "date")
newEntry.setValue("Hello World", forKey: "name")

do {
    try context.save()
} catch {
    print("Failed saving")
}
2

There are 2 answers

1
Chris On
class Data: ObservableObject {

    @Published var dataToUpdate: Int 
}

struct UpdateView : View {

@EnvironmentObject data: Data


   ...body ... {
     // use data.dataToUpdate here
  }
}

Just define data like you see in this example. Then fill dataToUpdate whenever your database changes...of course this is just a simplified example with just an Int Value...

0
riverbayMark On

Don't know if you discovered an answer yet, but I had a similar problem using UserDefaults and an intent extension in a shared app group. The thing that finally worked for me was adding a call to load the items in the method below in the SceneDelegate:

func sceneWillEnterForeground(_ scene: UIScene) {
        // Called as the scene transitions from the background to the foreground.
        // Use this method to undo the changes made on entering the background.
        SiriResponseManager.shared.grabUD()
    }

In my example I am using a singleton that houses the method to load the items from the UserDefaults. This singleton class is an ObservableObject. In my first view that loads (ContentView in my case) I assign an ObservedObject variable to my singleton class and retrieve the @State variables in my singleton to populate the string in my view.

Hopefully this helps.