SwiftUI: List does not update automatically after deleting all Core Data Entity entries - swift

I know SwiftUI uses state-driven rendering. So I was assuming, when I delete Core Data Entity entries, that my List with Core Data elements gets refreshed immediately.
I use this code, which gets my Entity cleaned succesfully:
func deleteAll()
{
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = ToDoItem.fetchRequest()
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
let persistentContainer = (UIApplication.shared.delegate as! AppDelegate).persistentContainer
do {
try persistentContainer.viewContext.execute(deleteRequest)
} catch let error as NSError {
print(error)
}
}
To get the List in my View visually empty I have to leave the View afterwards (for example with " self.presentationMode.wrappedValue.dismiss()") and open it again. As if the values are still stored somewhere in the memory or something.
This is of course not user-friendly and I am sure I just oversee something that refreshes the List immediately.
Maybe someone can help.

The reason is that execute (as described in details below - pay attention on first sentence) does not affect managed objects context, so all fetched objects remains in context and UI represents what is really presented by context.
So in general, after this bulk operation you need to inform back to that code (not provided here) force sync and refetch everything.
API interface declaration
// Method to pass a request to the store without affecting the contents of the managed object context.
// Will return an NSPersistentStoreResult which may contain additional information about the result of the action
// (ie a batch update result may contain the object IDs of the objects that were modified during the update).
// A request may succeed in some stores and fail in others. In this case, the error will contain information
// about each individual store failure.
// Will always reject NSSaveChangesRequests.
#available(iOS 8.0, *)
open func execute(_ request: NSPersistentStoreRequest) throws -> NSPersistentStoreResult
For example it might be the following approach (scratchy)
// somewhere in View declaration
#State private var refreshingID = UUID()
...
// somewhere in presenting fetch results
ForEach(fetchedResults) { item in
...
}.id(refreshingID) // < unique id of fetched results
...
// somewhere in bulk delete
try context.save() // < better to save everything pending
try context.execute(deleteRequest)
context.reset() // < reset context
self.refreshingID = UUID() // < force refresh

No need to force a refresh, this is IMO not a clean solution.
As you correctly mentioned in your question, there are still elements in memory. The solution is to update your in-memory objects after the execution with mergeChanges.
This blog post explains the solution in detail under "Updating in-memory objects".
There, the author provides an extension to NSBatchDeleteRequest as follows
extension NSManagedObjectContext {
/// Executes the given `NSBatchDeleteRequest` and directly merges the changes to bring the given managed object context up to date.
///
/// - Parameter batchDeleteRequest: The `NSBatchDeleteRequest` to execute.
/// - Throws: An error if anything went wrong executing the batch deletion.
public func executeAndMergeChanges(using batchDeleteRequest: NSBatchDeleteRequest) throws {
batchDeleteRequest.resultType = .resultTypeObjectIDs
let result = try execute(batchDeleteRequest) as? NSBatchDeleteResult
let changes: [AnyHashable: Any] = [NSDeletedObjectsKey: result?.result as? [NSManagedObjectID] ?? []]
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [self])
}
}
Here is an update to your code on how to call it:
func deleteAll() {
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = ToDoItem.fetchRequest()
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
let persistentContainer = (UIApplication.shared.delegate as! AppDelegate).persistentContainer
do {
try persistentContainer.viewContext.executeAndMergeChanges(deleteRequest)
} catch let error as NSError {
print(error)
}
}
Some more info also here under this link: Core Data NSBatchDeleteRequest appears to leave objects in context.

Related

SwiftUI dictionary problem when checking for nil

I am new to Swift and SwiftUI and this has been driving me nuts.
I have a list of items coming from a database, each associated with a User ID.
I need to display each item together with some user data (which also comes from the database). I want to save on database calls for getting user data for those users who have already appeared in my list of items.
In order to do that, I create a dictionary of user data, which I populate with each new user. If a user id is already in the dictionary, I won't be querying the database and will instead be using the cached user data in the dictionary.
So that was the idea and it seemed pretty straightforward. So I wrote the following
Main loop in the View goes through the list of items:
ForEach(xlist, id: \.self) { xentry in
Text(myModel.dictUser[xentry.actor_id]?.FirstName ?? "")
}.onAppear(perform: {myModel.cacheUser(uid: xentry.actor_id)})
in myModel, I try to cache the user:
#MainActor class myModel: ObservableObject {
#Published var dictUser: [String: STuser] = [:]
func cacheUser(uid: String) {
Task.init {
do {
if nil == dictUser[uid] {
dictUser[uid] = try await GetUserInfoFromDB(uid:uid)
}
} catch {
}
}
}
}
This should work but doesn't: the nil == dictUser[uid] in some cases (normally after a couple of entries) fails to correctly evaluate. I can see in the debugger that dictUser[uid] is clearly not a nil and contains valid data, yet the execution continues onto GetUserInfoFromDB.
Not sure what I'm doing wrong here. Any help appreciated.
You are trying to load data from a relatively slow, asynchronous source (a database in this case) and you want to utilise a cache to avoid the overhead of repeated database calls for the same item. A sensible approach.
With the cached items, there are three possible states:
it's already been requested and is in the cache
it's not been requested yet
it's been requested but the request is still in progress (and so the data item isn't in in the cache)
The first case is easy: if it's in the cache return it. The second case is also seemingly straightforward - you need to request the item from the database.
The last scenario is more complicated - how do you record that a request has already been made but not yet returned, and then wait for it to return it's data and send it to all those items that have requested it.
The obvious construct to represent something that can be in multiple states is an enum. For example:
enum cacheEntry {
case complete
case inProgress
}
but it's not that simple: you also want to associate each state with the data it returns from the cache.
The complete case is easy - use an associated value of the data item, in your case STUser.
The inProgress case is more complex as you want it to record the asynchronous activity that is in progress and when that activity completes return it's data item. The way to handle this is to store the asynchronous task as the associated value. So your cache entry looks like this:
enum CacheEntry {
case complete(STUser)
case inProgress(Task<STUser, Error>)
}
var cache: [String: CacheEntry] = [:]
(Note: if you're not handling the error you can replace the Error in the generic with Never.)
The question then becomes how do you use this construct?
Create a method to query the cache that can work with the async nature of the operation:
func entry(for bid: String) async throws -> STUser {
if let cacheEntry = imageCache[uid] {
switch cacheEntry {
case let .inProgress(task):
return try await task.value. //wait for the task to complete then return it's completion value
case let .downloaded(stUser):
return stUser //the item is already in the cache so return it
}
}
//There is no entry in the cache for the bid at this point
// Therefore create a task to retrieve the data asynchronously
let task = Task {
try await GetUserInfoFromDB(uid:uid)
}
//and store the task in the cache against the `uid` ready for any subsequent requests
imageCache[url] = .inProgress(task)
//process the task for the initial request
do {
//wait for the task to complete and then access its returned value
let item = try await task.value
cache[uid] = .downloaded(item) //replacing the 'inProgress' entry in the cache with the
return item // and return the retrieved value
} catch {
//if error, delete entry from the cache and handle the error
imageCache[url] = nil
throw error
}
}
The final complication now is that you have a synchronous dictionary that is being updated by an asynchronous task, with the potential for data races. To overcome this wrap the whole cache type in an actor to ensure the access to the cache is coordinated.
actor Cache {
enum CacheEntry {...}
func entry(for bid: String) async throws -> STUser {...}
}

How do I wait for a download to complete before continuing?

I have this block of code. It fetches data from the API and adds it to a locationDetails array, which is part of a singleton.
private func DownloadLocationDetails(placeID: String) {
let request = AF.request(GoogleAPI.shared.getLocationDetailsLink(placeID: placeID))
request.responseJSON { (data) in
guard let detail = try? JSONDecoder().decode(LocationDetailsBase.self, from: data.data!),
let result = detail.result else {
print("Something went wrong fetching nearby locations.")
return
}
DownloadManager.shared.locationDetails.append(result)
}
}
This block of code is the block in question. I'm creating a caching system of sorts that only downloads new information and retains any old information. This is being done to save calls to the API and for performance gains. The line DownloadLocationDetails(placeID: placeID) is a problem for me because if I execute this line of code it will continue to loop over and over again using unnecessary API calls while waiting for the download to complete. How do I effectively manage this?
func GetLocationDetail(placeID: String) -> LocationDetail {
for location in locationDetails {
if location.place_id == placeID { return location }
}
DownloadLocationDetails(placeID: placeID)
return GetLocationDetail(placeID: placeID)
}
I expect this GetLocationDetail(....) to be called whenever a user interacts with an interface object, so how do I also ensure that the view that calls this is properly notified that the download is complete?
I attempted using a closure but I can't get it to return the way I'm wanting it to. I have a property on the singleton that I want to set this value so that it can be called globally. I am also considering using GCD but I'm not sure of the structure for that.
Generally the pattern for something like this is to store the request object you created in DownloadLocationDetails so you can check to see if one is active before making another call. If you only want to support one at a time, then it's as simple as keeping the bare reference to the request object, but you could make a dictionary of request objects keyed off the placeID (and you probably want to think about maximum request count, and queue up additional requests).
Then the trick is to get notified when the given request object completes. There are a couple ways you could do this, such as keeping a list of callbacks to invoke when it completes, but the easiest would probably be just to refactor the code a bit so that you always update your UI when the request completes, so something like:
private func DownloadLocationDetails(placeID: String) {
let request = AF.request(GoogleAPI.shared.getLocationDetailsLink(placeID: placeID))
request.responseJSON { (data) in
guard let detail = try? JSONDecoder().decode(LocationDetailsBase.self, from: data.data!),
let result = detail.result else {
print("Something went wrong fetching nearby locations.")
return
}
DownloadManager.shared.locationDetails.append(result)
// Notify the UI to refresh for placeID
}
}

How should I guarantee fetch results from a different thread in a nested contexts are up to date, when saves are done asynchronously in background?

I've read the following Behavior differences between performBlock: and performBlockAndWait:?
But wasn't able to find an answer to my question.
The following code is picked up from an RayWenderlich video. Specifically at 10:05 the code is something like this:
class CoreDataStack {
var coordinator : NSPersistentStoreCoordinator
init(coordinator: NSPersistentStoreCoordinator){
self.coordinator = coordinator
}
// private, parent, in background used for saving
private lazy var savingContext : NSManagedObjectContext = {
let moc = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
moc.persistentStoreCoordinator = coordinator
return moc
}()
lazy var mainManagedObjectedContext : NSManagedObjectContext = {
let moc = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
moc.parent = self.savingContext
return moc
}()
func saveMainContext() {
guard savingContext.hasChanges || mainManagedObjectedContext.hasChanges else {
return
}
mainManagedObjectedContext.performAndWait {
do {
try mainManagedObjectedContext.save()
}catch let error{
fatalError(error.localizedDescription)
}
}
savingContext.perform {
do {
try self.savingContext.save()
}catch let error{
fatalError(error.localizedDescription)
}
}
}
}
From what I understand what happens is that the main context just passes the changes to its parent context which is a private, background context. It does this synchronously.
Then the parent, private context, does the actual saving against sqlite in a background thread asynchronously. Long story short this helps us a lot with performance. But what about data integrity?!
Imagine if I was to do this:
let coredataManager = CoreDataStack()
coredataManager.saveMainContext() // save is done asynchronously in background queue
coredataManager.mainManagedObjectedContext.fetch(fetchrequest)
How can I guarantee that my fetch is reading the most recent and updated results?
If we do our writes asynchronously then isn't there a chance that another read at the same time could end up with unexpected results ie results of the save changes could or could not be there?
EDIT:
I've made an improvement with the code below. I can make my save take in a completionHandler parameter. But that doesn't resolve the entire problem. What if I'm making a fetchRequest from a mainQueue somewhere else that isn't aware that a save is happening at the same time?
enum SaveStatus{
case noChanges
case failure
case success
}
func saveMainContext(completionHandler: (SaveStatus -> ())) {
guard savingContext.hasChanges || mainManagedObjectedContext.hasChanges else {
completionHandler(.noChanges)
return
}
mainManagedObjectedContext.performAndWait {
do {
try mainManagedObjectedContext.save()
}catch let error{
completionHandler(.failure)
fatalError(error.localizedDescription)
}
}
savingContext.perform {
do {
try self.savingContext.save()
completionHandler(.succes)
}catch let error{
completionHandler(.failure)
fatalError(error.localizedDescription)
}
}
}
All calls to mainManagedObjectContext will be synchronous and therefore blocking. If you call saveMainContext() and immediately afterwards call mainManagedObjectedContext.fetch(fetchrequest), the fetch request will not go through until the save request is completed, even if the save/fetch requests come from different queues (see the paragraph on FIFO in your link above).
When you perform a fetch request, you aren't pulling from the persistent storage - you're pulling from the child container, whom you just updated. You don't need to wait for the changes to be committed to the persistent storage, since you aren't accessing the data from there. The child container will give you the latest changes.
The child container is a container - it will hold your latest changes in memory (as opposed to stored on the disk - that is the persistent container's job).
The real issue here is that your CoreDataStack should implement the singleton pattern to prevent instantiating multiple versions of the same containers (that would still technically be on the same thread and therefore serialized, but accessing the containers wouldn't be thread safe). In other words, each time you instantiate CoreDataStack(), you're creating a new savingContext and mainManagedObjectedContext.
Instead, instantiate it just once.
class CoreDataStack {
var coordinator: NSPersistentStoreCoordinator
public static let sharedInstance = CoreDataStack()
private override init() {
self.coordinator = NSPersistantStoreCoordinator()
}
...
rest of your code here
...
}
And call like this:
CoreDataStack.sharedInstance.saveMainContext()
(See this link re: 'does the child have the same objects as the parent?')
The only case where a child would not be synced up with the parent is where you have multiple children accessing the same parent - but that doesn't seem to be the case here.
The question isn't specific to core-data.
It's the classic read-write question.
The common approach with protecting a datasource is to access your datasource using a serial queue. Otherwise yeah without the serial queue you will have a read-write problem.
In the following example:
let coredataManager = CoreDataStack() // 1
coredataManager.saveMainContext() // 2 save is done asynchronously in background queue
coredataManager.mainManagedObjectedContext.fetch(fetchrequest) // 3
coredataManager is to be accessed from a serial queue. So even if the write in the 2nd line is done asynchronously, the read at line 3, will have to wait until the serial queue is unblocked.

How can you create Results after creating records?

I have a method that should return Results, either by successfully querying, or by creating the records if they don't exist.
Something like:
class MyObject: Object {
dynamic var token = ""
static let realm = try! Realm()
class func findOrCreate(token token: String) -> Results<MyObject> {
// either it's found ...
let tokenResults = realm.objects(MyObject.self).filter("token = '\(token)'")
if !tokenResults.isEmpty {
return tokenResults
}
// ... or it's created
let newObject = MyObject()
newObject.token = token
try! realm.write {
realm.add(newObject)
}
// However, the next line results in the following error:
// 'Results<_>' cannot be constructed because it has no accessible initializers
return Results(newObject)
}
}
Maybe I should just be returning [MyObject] from this method. Is there any benefit to trying to keep it as Results instead of Array? I guess I'd lose any benefit of postponed evaluation since I'm already using isEmpty within the method, correct?
Results is an auto-updating view into underlying data in a Realm, which is why you can't construct it directly. So instead of return Results(newObject), you should return tokenResults, which will contain your newly added object, again because Results is an auto-updating view.

Replaced List<T> object not persisting consistently in Realm

I have a List<Workout> object that occasionally needs to be sorted (e.g., if a user adds a Workout out of order), but I can't seem to get the new sorted List<Workout> to persist. My code works the moment it runs (i.e., it shows up on the view as sorted), but when I exit the ViewController or restart the app, I see nothing. The nothing is due to the exercise.workoutDiary.removeAll() persisting, but apparently the subsequent assignment to the exercise.workoutDiary = sortedWorkoutDiary is not persisting. Any ideas why?
Everything else works just fine. The typical recordWorkout() case works assuming nothing is entered out of order. So the persisting is working in nearly all cases except for this overwrite of the sorted List.
The update happens here:
struct ExerciseDetailViewModel {
private let exercise: Exercise!
func recordWorkout(newWorkout: Workout) {
let lastWorkout = exercise.workoutDiary.last // grab the last workout for later comparison
let realm = try! Realm()
try! realm.write {
exercise.workoutDiary.append(newWorkout) // write the workout no matter what
}
if let secondToLastWorkout = lastWorkout { // only bother checking out of order if there is a last workout...
if newWorkout.date < secondToLastWorkout.date { // ...and now look to see if they are out of order
let sortedWorkoutDiary = exercise.sortedWorkouts
try! realm.write {
exercise.workoutDiary.removeAll()
exercise.workoutDiary = sortedWorkoutDiary
}
}
}
}
}
final class Exercise: Object {
var workoutDiary = List<Workout>()
var sortedWorkouts: List<Workout> {
return List(workoutDiary.sorted("date"))
}
}
final class Workout: Object {
dynamic var date = NSDate()
var sets = List<WorkSet>()
}
List<T> properties in Realm Swift must be mutated in place, not assigned to. The Swift runtime does not provide any way for Realm to intercept assignments to properties of generic types. Instead, you should use methods like appendContentsOf(_:) to mutate the List<T>:
exercise.workoutDiary.removeAll()
exercise.workoutDiary.appendContentsOf(sortedWorkoutDiary)
This limitation on assignment to properties of generic types is why the Realm Swift documentation recommends that you declare such properties using let rather than var. This will allow the Swift compiler to catch these sorts of mistakes.
One further note: for your sortedWorkouts computed property, it'd be preferable for it to return Results<Workout> instead to avoid allocating and populating an intermediate List<Workout>.