Why would there be inconsistency when saving NSManagedObjectContext after adding NSSecureUnarchiveFromDataTransformer?

82 views Asked by At

I have an app that uses Core Data to persist a store of Events.

An Event has an optional location (stored as a CLLocation) as one of its attributes. In my model, this location attribute has the type Transformable:

location attribute of Transformable type

My app has been in production for several years and everything's been working reliably, but some time in the past year I started getting an error in the Xcode console telling me I should switch to using "NSSecureUnarchiveFromData" or a subclass of NSSecureUnarchiveFromDataTransformer instead.

After doing some research (I'd consider myself a novice at Core Data) I determined I should write a NSSecureUnarchiveFromDataTransformer subclass and put the name of that class in the Transformer field for the location attribute, which was blank, with the Value Transformer Name placeholder text:

blank Transformer field

From what I found online, the subclass could be pretty straightforward for a Transformable attribute that contains a CLLocation:

@objc(CLLocationValueTransformer)
final class CLLocationValueTransformer: NSSecureUnarchiveFromDataTransformer {
    
    static let name = NSValueTransformerName(rawValue: String(describing: CLLocationValueTransformer.self))
    
    override static var allowedTopLevelClasses: [AnyClass] {
        return [CLLocation.self]
    }
    
    public static func register() {
        let transformer = CLLocationValueTransformer()
        ValueTransformer.setValueTransformer(transformer, forName: name)
    }
}

So, I made this subclass in my project and put the class name in the Transformer field for the location attribute:

filled-in Transformer field


But now, here's the problem: Once I started using my app after implementing the Transformer, I started getting unpredictable results.

Sometimes, when I created a new Event, it disappeared at the next app launch. It was not persisted by Core Data across app launches, like it was before the change.

Sometimes Events were saved, but sometimes they were not.

I couldn't figure out a clear pattern. They were not always saved if the location was included when it was first created, but sometimes they were. It seemed like other times the original Event was saved, but without location if the location was added later.

I've left out the boilerplate Core Data code, but basically I have baseManagedObjectContext, which is the layer that's connected to the NSPersistentStoreCoordinator to save data to disk.

baseManagedObjectContext is a parent to mainObjectContext, which is used by most of my UI. Then I create private contexts to write changes to first before saving them.

Here is example code to create a new Event with a possible location and save it, which was working consistently for years before adding the NSSecureUnarchiveFromDataTransformer subclass as the Transformer on location. I added fatalError for debugging, but it was never called, even when my data didn't fully save to disk:

private func addEvent(location: CLLocation?) {
    
    let privateLocalContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
    privateLocalContext.parent = coreDataStack.mainObjectContext
    privateLocalContext.undoManager = nil
    
    let entity = NSEntityDescription.entity(forEntityName: "Event",
                                            in: privateLocalContext)
    
    let newEvent = Event(entity: entity!,
                         insertInto: privateLocalContext)
    
    if let location = location {
        newEvent.location = location
    }
    
    privateLocalContext.performAndWait {
        do {
            try privateLocalContext.save()
        }
        catch { fatalError("error saving privateLocalContext") }
    }
    
    coreDataStack.mainObjectContext.performAndWait {
        do {
            try coreDataStack.mainObjectContext.save()
        }
        catch { fatalError("error saving mainObjectContext") }
    }
    
    coreDataStack.baseManagedObjectContext.perform {
        do {
            try coreDataStack.baseManagedObjectContext.save()
        }
        catch { fatalError("error saving baseManagedObjectContext") }
    }
}

With further debugging, I found that sometimes, even if the change was making it to mainObjectContext, it was not making it all the way to baseManagedObjectContext, or to disk. I created a whole separate Core Data stack for testing to read directly from disk.

This issue with the saves not propogating (like they did before, for years) is what was causing the data to not persist across app launches, but I do not understand why this would suddenly start happening after my addition of the Transformer on location.

What am I missing here with how Core Data works?

I did not think I was fundamentally changing anything when I switched from a blank Transformer field to a subclass of NSSecureUnarchiveFromDataTransformer, but clearly something is going on that I don't understand.

I'd like to adopt NSSecureUnarchiveFromDataTransformer, since Apple is recommending it. How can I change what I'm doing to be able to adopt it and have data save consistently?

For now, I've switched back to the blank Transformer field to keep things working like they did before.

0

There are 0 answers