0

I've come across an issue which rarely happens, and (of course) it works perfectly when I test it myself. It has only happened for a few users, and I know I have at least a couple hundred who use the same App daily.

The issue

When updating a list of coredata objects in a tableview, it not only updates the objects (correctly), it also creates duplicates of all these objects.

Coredata setup

It's a NSPersistentCloudKitContainer with these settings: container.viewContext.automaticallyMergesChangesFromParent = true container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy

Tableview setup

I have a tableview which displays a list of the 'ActivityType' objects. It's very simple, they have a name (some other basic string/int properties), and an integer called 'index'. This 'index' exists so that users can change the order in which they should be displayed. Here is some code for how each row is setup:

for activityType in activityTypes { 
    row = BlazeRow()
    row.title = activityType.name   
    row.cellTapped = {
        self.selectedActivityType(activityType)
    }
    row.object = activityType   
    
    row.cellReordered = {
        (index) in
        self.saveNewOrder()
    }
    section.addRow(row)
}

As you can see, it has 2 methods. One for selecting the activity which shows its details in a new viewcontroller, and one which is called whenever the order is changed.

Here's the method that is called whenever the order is changed:

func saveNewOrder() {

    Thread.printCurrent()

    let section = self.tableArray[0] as! BlazeSection
    for (index, row) in section.rows.enumerated() {
        let blazeRow = row as! BlazeRow
        let object = blazeRow.object as! ActivityType        
        object.index = Int32(index)
    }

    BDGCoreData.saveContext()
}

And here's the code that saves the context (I use a singleton to easily access the viewcontext):

class func saveContext(context: NSManagedObjectContext = BDGCoreData.viewContext) {
    if(context.hasChanges) {        
        do {
            try context.save()
        } catch {
            let nserror = error as NSError
            fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
        }
    }
}

Now, I swear to god it never calls the method in this viewcontroller to create a new object: let activity = ActivityType(context: BDGCoreData.viewContext). I know how that works, and it truly is only called in a completely different view controller. I searched for it again in my entire project just in case, and it's really never called/created in any other places.

But somehow, in very rase cases, it saves the correct new order but also duplicates all objects in this list. Since it only happens rarely, I thought it might have something to do with threads? Which is why, as you can see in the code, I printed out the current thread, but at least when testing on my device, it seems to be on the main thread.

I'm truly stumped. I have a pretty good understanding of coredata and the app itself is quite complex with full of objects with different kind of relationships.

But why this happens? I have no clue... Does anyone have an idea?

Bob de Graaf
  • 2,630
  • 1
  • 25
  • 43
  • Does it duplicate the number of objects in the table view (memory) only or also in your Core Data store? – Joakim Danielson Oct 31 '22 at 09:36
  • Welcome to SO - Please take the [tour](https://stackoverflow.com/tour) and read [How to Ask](https://stackoverflow.com/help/how-to-ask) to improve, edit and format your questions. Without a [Minimal Reproducible Example](https://stackoverflow.com/help/minimal-reproducible-example) it is impossible to help you troubleshoot. – lorem ipsum Oct 31 '22 at 10:27
  • 1
    Have seen similiar perplexing results happen with certain combinations of the context properties `automaticallyMergesChangesFromParent` and `mergePolicy`. Default vals can create duplicates if multiple instances of the app running on the same host. Or when running normally on multiple hosts and with data sync using `CloudKit` (or similiar) - particularly in unusually intermittent connection environment. Not enough info on this app's Context setup and overall capabilities, but fwiw those'd be my chief suspects. Good luck. – shufflingb Oct 31 '22 at 10:28
  • @JoakimDanielson In the coredata store, (and after a reload in the list as well, of course). But the coredata store is what baffles me. – Bob de Graaf Oct 31 '22 at 13:33
  • @shufflingb Thanks for your comment. It is indeed a cloudkit setup, and my users successfully use it on both devices where the sync is running smoothly. I will ask this user if he uses it on multiple devices. I don’t really understand what you mean with ‘multiple instances running on the same host’? That’s impossible on an iPhone/iPad right? – Bob de Graaf Oct 31 '22 at 13:36
  • You didn't mention CloudKit in the question but maybe this is to do with the synchronisation then, on the server you have object `a` with index 1 but locally object `a` has index 2 so they are considered to be 2 different objects. Just a general idea, I haven't used CloudKit much myself so I don't know the details of the synch logic. – Joakim Danielson Oct 31 '22 at 14:00
  • @JoakimDanielson Good point, I’ll update my post with some more details – Bob de Graaf Oct 31 '22 at 14:19
  • Yes on iOS, ipadOS under normal circumstance should only be processes associated with single instance. – shufflingb Oct 31 '22 at 16:12
  • @shufflingb I suspect you might be right about your 'chief suspects' :) On the other hand, I checked with my user that has this issue, but he doesn't use another device, only his iPhone. Do you have any ideas on how to go forward/tackle this issue? – Bob de Graaf Nov 01 '22 at 11:05
  • That's rough. Afraid no direct experience debugging anything like this. But fwi - and only if not already tried - would probably start with disconnected from internet and iCloud, check for disk space and memory issues. and to verify installation doesn't have corruption or bogus locks in the local backend (SQLite) DB for CoreData. Would hope something in that lot might find something interesting, otherwise stumped! – shufflingb Nov 01 '22 at 14:55
  • @BobdeGraaf In `cellReordered` and `cellTapped` can you try using a weak reference `[weak self] in self?`. It might be your cells and view controller have strong circular references, and that your view controller when dismissed is not deallocated. So when you create the view controller the second time and call save a new set of data is `updated + saved`. It's a long shot. – mani Nov 04 '22 at 13:54
  • 1. Edit Scheme > Run > Arguments > Arguments Passed on Launch: Add the following: `-com.apple.CoreData.ConcurrencyDebug 1` and `-com.apple.CoreData.MigrationDebug 1`. 2. Ensure you are making CoreData changes within `perform(_:)` or `performAndWait(_:)`. 3. Do you have any attribute that is supposed to be in your app (example: employeeID)? If so can you print and see if there are multiple records with the same id? – user1046037 Nov 06 '22 at 19:30
  • @mani Interesting thought, I'll try to do that. The biggest issue is that I can't reproduce the bug, so anything I try, I have no idea whether that was the problem ;) But thanks for the idea! – Bob de Graaf Nov 07 '22 at 09:02
  • @user1046037 I checked that I use the main thread, but now I also added using the DispatchQueue.main.async in the saveContext (according to this: https://stackoverflow.com/questions/66742601/swift-5-difference-between-dispatchqueue-main-asyncafter-and-perform) the perform-methods are a more older legacy API. I will try to concurrency-debug arguments, good one. And as for the attributes, there is only 'index' and 'name', but I guess index could be called the ID. But like I said in other comments, I can't reproduce the bug so here the indexes are all okay and different :) – Bob de Graaf Nov 07 '22 at 09:06
  • `NSManagedObject` is not thread safe. When you compile and run your project using those launch arguments, your app will crash every time your code accesses it outside the context (`perform(_:)` or `performAndWait(_:)`). So then you can wrap all accesses inside the block. It might seem like a lot of work, but a few days of cleaning up in a separate branch could save you a ton of time in the long run – user1046037 Nov 07 '22 at 11:40
  • `perform(_:)` or `performAndWait(_:)` are on the `NSManagedObjectContext`. I think you are confusing them with `DispatchQueue`. Please read the documentation carefully, this is important while using Core Data – user1046037 Nov 07 '22 at 11:42
  • @user1046037 Ah but isn't that only applicable when you use additional queues like a private queue? I only use my fetches and changes on the main queue. – Bob de Graaf Nov 07 '22 at 15:10
  • @user1046037 Anyway, I also tested a lot of times now doing everything I can within my app with those arguments activated in my scheme, but it never crashes, not even once... – Bob de Graaf Nov 07 '22 at 15:53
  • 1
    @user1046037 I've read a bit more about your perform methods because I was pretty sure that I didn't need them in my App. I believe that according to this guide: (https://www.kairadiagne.com/2019/01/06/understanding-the-core-data-perform-methods.html) I'm right about my app: "When interacting with a context or its managed objects always use a perform method to make sure that the work is executed on the right queue. An exception to this rule is when you want to access a view context and you are already on the main queue. In that case you don’t need to use one of the perform methods." – Bob de Graaf Nov 07 '22 at 16:06
  • On a different note, don't use index as an id, because order could be changed on a different device and things could be synced. Use an internal field which you generate and is not visible to the user. – user1046037 Nov 07 '22 at 22:23
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/249416/discussion-between-bob-de-graaf-and-user1046037). – Bob de Graaf Nov 08 '22 at 08:06
  • Some reasons for duplication are given in [this post](https://atomicbird.com/blog/icloud-complications-part-2/). In your case, the duplicates are apparently stored in the persistent store by CloudKit sync. If the persistent store is changed by CloudKit sync, the persistent store sends a `.NSPersistentStoreRemoteChange` notification. Apple uses it in their [sample app](https://developer.apple.com/documentation/coredata/synchronizing_a_local_store_to_the_cloud) to process the transaction history of the persistent store and to de-duplicate data. Do you something similar? – Reinhard Männer Nov 10 '22 at 16:55
  • @mani I found out I did have a retain cycle. Like you said, it's a long shot, and since I can't reproduce the issue I have no idea if that was the cause. But I did find the retain cycle and since their are no other answers and the bounty expires, I'm willing to give you the bounty if you post is as the answer so it's not wasted. You only have 2 hours left though before it expires ;) – Bob de Graaf Nov 11 '22 at 07:51
  • @BobdeGraaf glad i could help. Hopefully this resolved it. – mani Nov 11 '22 at 16:28

0 Answers0