There are many questions on @FetchRequest with answers that provide workarounds for forcing SwiftUI to redraw the view. I can't find any that address this question.
My app uses a share extension to insert a new item into my CoreData
model (sqlite). A framework is shared between the main app and the share extension target. The main SwiftUI ContentView
in my app uses a @FetchRequest
property to monitor the contents of the database and update the view:
struct ContentView: View {
@FetchRequest(fetchRequest: MyObj.allFetchRequest()) var allObjs: FetchedResults<MyObj>
...
}
my xcdatamodel is extended with:
public static func allFetchRequest() -> NSFetchRequest<MyObj> {
let request: NSFetchRequest<MyObj> = MyObj.fetchRequest()
// update request to sort by name
request.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
return request
}
The share extension adds a new item using:
let newObj = MyObj(context: context)
newObj.name = "new name"
try! context.save()
After adding a new item using the share extension, when I return to my app by switching apps on the device, SceneDelegate
detects sceneDidBecomeActive
. In this function, I can manually run a fetch request and see that the new item has been added. I can even use answers from other SO questions to trick the UI into rebuilding in ContentView (by toggling a Bool used in the builder on receipt of a notification), but in either case, when the UI rebuilds, allObjs
does not contain the updated results from the database.
In my sceneDidBecomeActive
I have tried numerous combinations of code to attempt to update the context with the latest contents of the database in a way that the @FetchRequest
will see it. The data is there, and the my managed context can see this new data because a manual fetch request returns the new item, but the @FetchRequest
is always stale preventing a UI rebuild. In both locations I've printed the context and am sure that it is the same context. I've tried:
context.reset()
context.refreshAllObjects()
context.save()
I'm at a loss as to why the @FetchRequest
is not updating its values when ContentView
redraws.
How can I force @FetchRequest
to update its objects and cause a SwiftUI rebuild after my share extension adds a new object to the database?
(Xcode 11.4.1, Swift 5, target: iOS 13.1)
edit:
In my ContentView
, I print the allObjs
variables, their state, the memory address of the context, etc. I compare this to a new fetch from the context and all values for existing objects are the same, except the new fetch shows the newly added object, whereas allObjs
does not contain the new object. It is very clear that the @FetchRequest
object is not getting updated.
edit 2:
I tried creating a custom ObservableObject
class with a @Published
property and an explicit update()
method, then using it as an @ObservedObject
in ContentView
. This works.
class MyObjList: ObservableObject {
static let shared = MyObjList()
@Published var allObjs: [MyObj]!
init() {
update()
}
func update() {
allObjs = try! DataStore.persistentContainer.viewContext.fetch(MyObj.allFetchRequest())
}
}
Then in ContentView
:
@EnvironmentObject var allObjs: MyObjList
Which helps confirm that the data is there, it just isn't getting updated properly by @FetchRequest
. This is a functional workaround.
I solved a similar problem using persistent history tracking and remote change notifications. These make Core Data record every change that occurs to an SQLite store and sends you notifications whenever one of these changes occurred remotely (eg from an extension or iCloud).
These features aren't enabled by default. You have to configure them before loading the persistent stores:
persistentStoreDescriptions.forEach {
$0.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
$0.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
}
Then subscribe to remote change notifications:
NotificationCenter.default.addObserver(
/* The persistent container/view model/MyObjList/etc that acts as observer */,
selector: #selector(coordinatorReceivedRemoteChanges(_:)),
name: .NSPersistentStoreRemoteChange,
object: persistentStoreCoordinator // don't forget to pass the PSC as the object!
)
Now we will be notified when remote changes occur, but we need to share these changes with the managed object contexts throughout the app. This looks different for every app: sometimes your MOCs don't need to know about the changes at all, sometimes they only need to know about some of them, sometimes all of them. Apple recently published a guide that goes into depth about how to ensure only relevant changes are processed.
In this scenario you might do:
@objc
func coordinatorReceivedRemoteChanges(_ notification: Notification) {
let currentToken = notification.userInfo?[NSPersistentHistoryTokenKey] as? NSPersistentHistoryToken
let existingToken = ... // retrieve the previously processed history token so we don't repeat work; you might have saved it to disk using NSKeyedArchiver
// Fetch the history
let historyRequest = NSPersistentHistoryChangeRequest.fetchHistory(after: existingToken)
let result = try! context.execute(historyRequest) as! NSPersistentHistoryResult
let transactions = result.result as! [NSPersistentHistoryTransaction]
// Filter out the non-relevant changes
for transaction in transaction {
// Maybe we only care about changes to MyObjs
guard transaction.entityDescription.name == MyObj.entity().name else { continue }
// Maybe we only care about specific kinds of changes
guard let changes = transaction.changes, changes.contains(where: { ... }) else { continue }
// Merge transaction into the view context
viewContext.mergeChanges(fromContextDidSave: transaction.objectIDNotification())
}
// Finally, save `currentToken` so it can be used next time and optionally delete old history.
}
Any @FetchRequest
s using viewContext
will now receive all merged changes and refresh their data accordingly.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With