Siri Shortcuts with SwiftUI and Core Data

Hello, I have created a simple SwiftUI app with Core Data and want to be able to add data via the shortcuts app, I have implemented Intents and the IntentHandler class. When I create a shortcut to add data to my app and run it, nothing happens in the app, the list does not refresh, the only way to see the added data is to close the app completely and reopen it. How can I refresh the UI immediately? I will post my Core Data stack and my SwiftUI view.

struct PersistenceController {
    static let shared = PersistenceController()

    let container: NSPersistentContainer

    init() {
        container = NSPersistentContainer(name: "SiriShort")

        guard let fileContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.SiriShortcut2")?.appendingPathComponent("SiriShort.sqlite") else {
            fatalError("Shared file container could not be created.")
        }
        
        let storeDescription = NSPersistentStoreDescription(url: fileContainer)
        storeDescription.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
        storeDescription.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
        
        container.persistentStoreDescriptions = [storeDescription]
        
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
        
        container.viewContext.automaticallyMergesChangesFromParent = true
        container.viewContext.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy
        
    }
}

View:

import SwiftUI
import CoreData
import Intents

struct ContentView: View {
    @Environment(\.managedObjectContext) private var viewContext
        
    @State private var view: Bool = false
        
    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \Item.text, ascending: true)],
        animation: .default)
    private var items: FetchedResults<Item>

    var body: some View {
        NavigationView {
            List {
                ForEach(items) { item in
                    Text(item.text!)
                }
                .onDelete(perform: deleteItems)
            }

            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    EditButton()
                }
                ToolbarItem {
                    Button(action: addItem) {
                        Label("Add Item", systemImage: "plus")
                    }
                }
            }
        }
    }

    private func addItem() {
        withAnimation {
            let newItem = Item(context: viewContext)
            newItem.text = "\(Int.random(in: 0...1000))"
            
            do {
                try viewContext.save()
            } catch {
                // 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.
                let nsError = error as NSError
                fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
            }
            
            makeDonation(text: newItem.text!)
        }
    }

    func makeDonation(text: String) {
        let intent = MakeUppercaseIntent()
        
        intent.text = text
        intent.unit = "1"
        intent.suggestedInvocationPhrase = "Add \(text) to app"

        let interaction = INInteraction(intent: intent, response: nil)
        
        interaction.donate { (error) in
            if error != nil {
                if let error = error as NSError? {
                    print("Donation failed: %@" + error.localizedDescription)
                }
            } else {
                print("Successfully donated interaction")
            }
        }
    }

    
    private func deleteItems(offsets: IndexSet) {
        withAnimation {
            offsets.map { items[$0] }.forEach(viewContext.delete)

            do {
                try viewContext.save()
            } catch {
                // 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.
                let nsError = error as NSError
                fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
            }
        }
    }
}

I tried to observe NSPersistentStoreRemoteChange like this:.onReceive(NotificationCenter.default.publisher(for: .NSPersistentStoreRemoteChange)) { notification in DispatchQueue.main.async { let predicate = items.nsPredicate items.nsPredicate = nil items.nsPredicate = NSPredicate(value: true) items.nsPredicate = predicate } }

but the operation that I execute in the closure isn't efficient and gives me an issue: Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates

Did you tried to publish change in the main thread? (DispatchQueue.main.async { // changes here..//}). Actually I have the same problem here. I need to update the list in the App after that new items has been added via Shortcut.

Short Answer: It is a bug. Just run (command + R) your app then stop it. Then talk to your Siri. You will see the new Item is added to your list. I think debugger stops core data to be updated from external events.

Long Answer: I also tried many different approaches in the web and nothing worked. But here is the simplest code that I could add item to my list via Siri.

  1. Simply setup your PersistenceController
public struct PersistenceController {
    public static let shared = PersistenceController()
    public let container: NSPersistentCloudKitContainer

    init(inMemory: Bool = false) {
        container = NSPersistentCloudKitContainer(name: "YourApp.")
        if inMemory {
            container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
        }
        else {
            let storeURL = AppGroup.coredata.containerURL.appendingPathComponent("YourApp.sqlite")
            let description = NSPersistentStoreDescription(url: storeURL)
            container.persistentStoreDescriptions = [description]
        }

        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
    }

    public func save() {
        let context = container.viewContext
        if context.hasChanges {
            do {
                try context.save()
            } catch {
                // Show some error here
                print("Error saving data...")
            }
        }
    }
}

  1. Define your fetchRequest in your view
// your listView

    @Environment(\.managedObjectContext) private var viewContext

    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \item.name, ascending: true)],
        animation: .default)
    var items: FetchedResults<Items>

            // use items in your list
            List {
                ForEach(items,  id: \.self) { item in
                    NavigationLink {
                        ItemDetailsView(selectedItem: item)
                    } label: {
                        ListItemRow(item: item) // <- item should be @ObservedObject
                    }
                }
            }

  1. Define your item as @ObservedObject in ListItemRow

As simple as that!

I've had this warning ("Publishing changes from background threads is not allowed") as well while I was using .onReceive(NotificationCenter.default.publisher(for: .NSPersistentStoreRemoteChange)). To avoid this problem let this publisher receive notifications on a main thread like this: .onReceive(NotificationCenter.default.publisher(for: .NSPersistentStoreRemoteChange).receive(on: DispatchQueue.main). I can't say would it affect the UI smoothness - in my case it is not.

Siri Shortcuts with SwiftUI and Core Data
 
 
Q