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:
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:
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:
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 Event
s 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.