💾 Archived View for dyn.fussycoder.ninja › personal › apple › coredata.gmi captured on 2022-07-16 at 14:23:46. Gemini links have been rewritten to link to archived content
-=-=-=-=-=-=-
There are a number of different databases that can be used to save data on an iOS device.
CoreData will basically manage everything - the types, relationships, lifetimes, everything.
It will also manage iCloud sync'ing on modern systems.
From the CoreData Documentation, it also has the following features:
It appears to be significantly improved in more recent versions of xcode and iOS SDKs, but has some quirks, mentioned near the end.
Refer to the following to learn how to create the model:
The CoreData model is defined in an "xcdatamodeld" file, which includes the full schema - including the types, properties, and relationships.
Note: The original name for the file is the name that XCode will use when selecting a model name for code generation.
Once the model has been defined, the classes in code also need to be defined. These are defined as part of the "Core Data Stack", described here:
To summarize that page, the classes consist of the Persistent Container (NSPersistentContainer), which manages the following:
It is intended that the "container" is passed to the user interface, and the above link in "Setting up a Core Data Stack" describes how you can do that.
Not really mentioned here is the code generation, I just go with the defaults and generate the classes, but there is more information at:
let container = NSPersistentCloudKitContainer(name: "DatabaseModel") container.loadPersistentStores(completionHandler: { _, err in if let err = err as NSError? { fatalError("Unresolved: \(err)") } })
Creating objects seems to be simple, here, we create an instance of a type defined in the "xcdatamodeld" Model file as "CDItem":
let item = CDItem(context: container.viewContext) item.searchTerms = "hey there" item.dataType = forType item.pbmessage = data
All one has to do, is save the context:
try! container.viewContext.save()
Queries are done using "NSFetchRequest", and in particular, using the "NSFetchRequest.predicate" property, which is an "NSPredicate" type.
"NSPredicate" is documented at:
These are not specific to CoreData, they are a general framework for specifying queries in Cocoa, so you must take care that they work with CoreData specifically in this context, so be wary when reading NSPredicate documentations.
This means that the following does not work when used with CoreData:
The provided code example from the "Predicate Programming Guide" is as follows:
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"(lastName like[cd] %@) AND (birthday > %@)", lastNameSearchString, birthdaySearchDate];
Anecedotally, it seems that CoreData does not create indexes for each property in a coredata model, so when there are huge numbers of items, performance is a little bit slower than it should be.
Unfortunately, I can't find any information about how to do indexing, and what information I have found (listed below), are obsolete and no-longer appear to be valid:
artandlogic.com: Optimising Core Data Searches and Sorts
As an update, I've since realised that the XCode "editor" menu shows additional actions that can be done that are not obvious in the main xcode view nor the 'info' view for the event, and there, it appears that an index can be created however I'm not sure how to use these indexes as yet. Naively creating one does not result in an SQLite Index, and it does not appear to be used by default with NSPredicate searches.
My feeling is that CoreData will probably create and manage indexes for you more or less automatically once you model your data appropriately, however in my test application I have one simple type and there are no relations - if I was making real use of CoreData, I should create a variety of entities that properly represent the data I am storing, with relationships defined, and CoreData would then probably set up the appropriate indexes.
(In addition, I'm not even using the CoreData object identifiers properly - I'm using my own "identifier" attribute, which is again probably not what Core Data is expecting.)
The CoreData FAQ mentions that this can be achieved "for free" by implementing this on the Window Delegate:
func windowWillReturnUndoManager(window: NSWindow) -> NSUndoManager? { return managedObjectContext!.undoManager }
This is very easy, see the medium article "Syncing Data on iOS devices with CoreData and CloudKit". It appears that there are only two, maybe three or four things required:
1. In the model's configuration, tick "Used with CloudKit" in the item properties.
2. In the code, use the "NSPersistentCloudKitContainer" instead of the "NSPersistentContainer"
3. Add the correct capabilities in the XCode Project's Signing & Capabilities section
4. And optionally enable Background Modes capabilities for background sync'ing. For this last bit, some code is also required:
static var viewContext: NSManagedObjectContext { let viewContext = persistentContainer.viewContext viewContext.automaticallyMergesChangesFromParent = true return viewContext }
Note: Once a container ID has been created, it can never be deleted - it will forever remain on your CloudKit Dashboard.
When it's integrated, the container can be visualised in the CloudKit dashboard.
CoreData is used on multiple devices, and if iCloud data sync has been opted in, then there will be a need to merge data.
This is described in the CoreData Change Management page.
It's possible to have the same object modified at the same time on these devices.
The default policy is defined by the "NSErrorMergePolicy" property, and causes a safe to fail if there is a conflict, the error's "userInfo" property is a dictionary that will contain "conflictList", which is an array of conflict records.
This allows the application to present the error and conflict to the user to resolve.
Alternatively, there are other policies:
The medium article "Advanced Coredata Debug and Query" mentions that you can enable SQL tracing by launching the application with the following arguments:
-com.apple.CoreData.SQLDebug 1
Although CoreData is generally implemented in terms of an SQLite database, you can not create or use any of the SQLite API when interacting with the CoreData database as this is completely unsupported by Apple.
When I investigated CoreData, I ended up with a crash during large updates:
2021-10-28 20:26:45.567260+1100 Database[8039:329888] [error] error: Serious application error. Exception was caught during Core Data change processing. This is usually a bug within an observer of NSManagedObjectContextObjectsDidChangeNotification. -[__NSCFSet addObject:]: attempt to insert nil with userInfo (null) CoreData: error: Serious application error. Exception was caught during Core Data change processing. This is usually a bug within an observer of NSManagedObjectContextObjectsDidChangeNotification. -[__NSCFSet addObject:]: attempt to insert nil with userInfo (null) 2021-10-28 20:26:45.569700+1100 Database[8039:329888] [General] An uncaught exception was raised 2021-10-28 20:26:45.570080+1100 Database[8039:329888] [General] -[__NSCFSet addObject:]: attempt to insert nil 2021-10-28 20:26:45.570127+1100 Database[8039:329888] [General] ( 0 CoreFoundation 0x000000019098b838 __exceptionPreprocess + 240 1 libobjc.A.dylib 0x00000001906b50a8 objc_exception_throw + 60 2 CoreFoundation 0x0000000190a576f8 -[__NSCFString characterAtIndex:].cold.1 + 0
It appears that in my code, I was not paying attention to the thread. This brings us to the Concurrency topic:
In addition, most errors appear to be runtime errors; you can't catch them easily.
The core data context can only be used within the thread it was created in, there are two concurrency types:
To quote the "Core Data, Multithreading, and the Main Thread" article:
When you are using an NSPersistentContainer, the viewContext property is configured as a NSMainQueueConcurrencyType context and the contexts associated with performBackgroundTask: and newBackgroundContext are configured as NSPrivateQueueConcurrencyType
In addition, the "NSManagedObject", which is the type for all your CoreData types, is not intended to be passed between queues - doing so can result in corruption or crashes.
Here is how I set up my 'moc' when using private queue concurrency:
private lazy var moc: NSManagedObjectContext = { let moc = NSManagedObjectContext(concurrencyType: NSManagedObjectContextConcurrencyType.privateQueueConcurrencyType) moc.parent = container.viewContext return moc }()
You can then use that by using:
moc.perform { ... Do your stuff with Core Data ... }
In no particular order:
Advanced Coredata Debug and Query
Syncing Data on iOS devices with CoreData and CloudKit
Core Data, Multithreading, and the Main Thread
In addition, the following may be helpful, but weren't used as a reference:
What's New in Core Data (Mainly about Core Spotlight, and indexing)