💾 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

View Raw

More Information

-=-=-=-=-=-=-

CoreData - Introduction

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.

Initializing

Creating the model.

Refer to the following to learn how to create the model:

Creating a Core Data 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:

Setting up a Core Data Stack

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:

Generating Code

Initialising the code

let container = NSPersistentCloudKitContainer(name: "DatabaseModel")
container.loadPersistentStores(completionHandler: { _, err in
    if let err = err as NSError? {
        fatalError("Unresolved: \(err)")
    }
})

Creating objects

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

Saving objects

All one has to do, is save the context:

try! container.viewContext.save()

Queries

Queries are done using "NSFetchRequest", and in particular, using the "NSFetchRequest.predicate" property, which is an "NSPredicate" type.

"NSPredicate" is documented at:

Predicate Programming Guide

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];

Performance

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.)

Undo/Redo

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
}

Cloudkit Integration

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.

Merge Policy

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:

Debugging

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.

Crashes

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.

Concurrency and Multithreading

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 ...
}

Quirks and irritations

In no particular order:

References

CoreData Documentation

CoreData FAQ

CoreData Change Management

Advanced Coredata Debug and Query

Syncing Data on iOS devices with CoreData and CloudKit

Core Data, Multithreading, and the Main Thread

Predicate Programming Guide

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)

TODO