Core data Notes

Aman Kesarwani
24 min readSep 9, 2023

--

Core Data is an object graph management and persistence framework in the macOS and iOS SDKs.

The first step is to create a managed object model, which describes the way Core Data represents data on disk.

  • An entity is a class definition in Core Data. The classic example is an Employee or a Company. In a relational database, an entity corresponds to a table.
  • An attribute is a piece of information attached to a particular entity. For example, an Employee entity could have attributes for the employee’s name, position and salary. In a database, an attribute corresponds to a particular field in a table.
  • A relationship is a link between multiple entities. In Core Data, relationships between two entities are called to-one relationships, while those between one and many entities are called to-many relationships. For example, a Manager can have a to-many relationship with a set of employees, whereas an individual Employee will usually have a to-one relationship with his manager.

You can think of a Core Data entity as a class definition and the managed object as an instance of that class.

The only way Core Data provides to read the value is key- value coding, commonly referred to as KVC.

cell.textLabel?.text =
person.value(forKeyPath: "name") as? String

KVC is a mechanism in Foundation for accessing an object’s properties indirectly using strings. In this case, KVC makes NSMangedObject behave somewhat like a dictionary at runtime.

Key-value coding is available to all classes inheriting from NSObject, including NSManagedObject. You can’t access properties using KVC on a Swift object that doesn’t descend from NSObject.

  1. NSFetchRequest is the class responsible for fetching from Core Data. Fetch requests are both powerful and flexible. You can use fetch requests to fetch a set of objects meeting the provided criteria (i.e. give me all employees living in Wisconsin and have been with the company at least three years), individual values (i.e., give me the longest name in the database) and more.
  2. Setting a fetch request’s entity property, or alternatively initializing it with init(entityName:), fetches all objects of a particular entity. This is what you do here to fetch all Person entities. Also note NSFetchRequest is a generic type. This use of generics specifies a fetch request’s expected return type, in this case NSManagedObject.

Data type in core data

• ABooleannamedisFavorite
• ADatenamedlastWorn
• ADoublenamedrating

  • AStringnamedsearchKey
    • An Integer 32 named timesWorn
  • • AUUIDnamedid
    • AURInamedurl

UUID before, it’s short for universally unique identifier and it’s commonly used to uniquely identify information.

URI stands for uniform resource identifier and it’s used to name and identify different resources like files and web pages. In fact, all URLs are URIs!

Core Data provides the option of storing arbitrary blobs of binary data directly in your data model. These could be anything from images, to PDF files, to anything that can be serialized into zeroes and ones.

For heavy Binary Data,When you enable Allows External Storage, Core Data heuristically decides on a per- value basis if it should save the data directly in the database or store a URI that points to a separate file.

The Allows External Storage option is only available for the binary data attribute type. In addition, if you turn it on, you won’t be able to query Core Data using this attribute.

Transformable attributes persist data types that are not listed in Xcode’s Data Model Inspector. These include types that Apple ships in their frameworks such as UIColor and CLLocationCoordinate2D as well as your own types.

You have to meet three requirements to make an attribute Transformable:

  1. Add NSSecureCoding protocol conformance to the backing data type.
  2. Create and register an NSSecureUnarchiveFromDataTransformer subclass.
  3. Associate the custom data transformer subclass with the Transformable attribute in the Data Model Editor.
  4. Override allowedTopLevelClasses to return a list of classes this data transformer can decode. We want to persist and retrieve instances of UIColor, so here you return an array that contains only that class.
  5. Like the name implies, the static function register() helps you register your subclass with ValueTransformer. But why do you need to do this? ValueTransformer maintains a key-value mapping where the key is a name you provide using NSValueTransformerName and the value is an instance of the corresponding transformer. You will need this mapping later in the Data Model Editor.
func application(_ application: UIApplication,
didFinishLaunchingWithOptions
launchOptions: [UIApplication.LaunchOptionsKey: Any]?)
-> Bool {
ColorAttributeTransformer.register()
return true
}

Here you register your data transformer with the static method you implemented earlier. Registration can happen at any point before your application sets up the Core Data stack.

// Set the name
person.setValue(aName, forKeyPath: "name")
// Get the name
let name = person.value(forKeyPath: "name")

In object-oriented parlance, an object is a set of values along with a set of operations defined on those values. In this case, Xcode separates these two things into two separate files. The values (i.e. the properties that correspond to the BowTie attributes in your data model) are in BowTie+CoreDataProperties.swift, whereas the operations are in the currently empty BowTie+CoreDataClass.swift.

  • String maps to String?
  • Integer 16 maps to Int16
  • Integer 32 maps to Int32
  • Integer 64 maps to Int64
  • Float maps to Float
  • Double maps to Double
  • Boolean maps to Bool
  • Decimal maps to NSDecimalNumber?
  • Date maps to Date?
  • URI maps to URL?
  • UUID maps to UUID?
  • Binary data maps to Data?
  • Transformable maps to NSObject?

Similar to @dynamic in Objective-C, the @NSManaged attribute informs the Swift compiler that the backing store and implementation of a property will be provided at runtime instead of compile time.

The normal pattern is for a property to be backed by an instance variable in memory. A property on a managed object is different: It’s backed by the managed object context, so the source of the data is not known at compile time.

To reiterate, before you can do anything in Core Data, you first have to get an NSManagedObjectContext to work with. Knowing how to propagate a managed object context to different parts of your app is an important aspect of Core Data programming.

var managedContext: NSManagedObjectContext!
  1. The way you store images in Core Data. The property list contains a file name for each bow tie, not the file image — the actual images are in the project’s asset catalog. With this file name, you instantiate the UIImage and immediately convert it into Data by means of pngData() before storing it in the imageData property.
  2. The way you store the color. Even though the color is stored in a transformable attribute, it doesn’t require any special treatment before you store it in tintColor. You simply set the property and you’re good to go.

Core Data supports validation for most attribute types out of the box.

Note: Normally, you have to version your data model if you want to change it after you’ve shipped your app.

Validation kicks in immediately after you call save() on your managed object context. The managed object context checks with the model to see if any of the new values conflict with the validation rules you’ve put in place.

If there’s a validation error, the save fails. Remember that NSError in the do-catch block wrapping the save method

Core Data supports different attribute data types, which determines the kind of data you can store in your entities and how much space they will occupy on disk. Some common attribute data types are String, Date, and Double.

The Binary Data attribute data type gives you the option of storing arbitrary amounts of binary data in your data model.

The Transformable attribute data type lets you store any object that conforms to NSSecureCoding in your data model.

Using an NSManagedObject subclass is a better way to work with a Core Data entity. You can either generate the subclass manually or let Xcode do it automatically.

You can refine the set entities fetched by NSFetchRequest using an NSPredicate.

You can set validation rules (e.g. maximum value and minimum value) to most attribute data types directly in the data model editor. The managed object context will throw an error if you try to save invalid data.

The stack is made up of four Core Data classes:

• NSManagedObjectModel
• NSPersistentStore
• NSPersistentStoreCoordinator

• NSManagedObjectContext

The managed object model

The NSManagedObjectModel represents each object type in your app’s data model, the properties they can have, and the relationships between them. Other parts of the Core Data stack use the model to create objects, store properties and save data.

NSManagedObjectModel as a database schema

Note: You may be wondering how NSManagedObjectModel relates to the data model editor you’ve been using all along. Good question!

The visual editor creates and edits an xcdatamodel file. There’s a special compiler, momc, that compiles the model file into a set of files in a momd folder.

Just as your Swift code is compiled and optimized so it can run on a device, the compiled model can be accessed efficiently at runtime. Core Data uses the compiled contents of the momd folder to initialize an NSManagedObjectModel at runtime.

The persistent store

NSPersistentStore reads and writes data to whichever storage method you’ve decided to use. Core Data provides four types of NSPersistentStore out of the box: three atomic and one non-atomic.

An atomic persistent store needs to be completely deserialized and loaded into memory before you can make any read or write operations. In contrast, a non- atomic persistent store can load chunks of itself onto memory as needed.

Here’s a brief overview of the four built-in Core Data store types:

  1. NSSQLiteStoreType is backed by an SQLite database. It’s the only non-atomic store type Core Data supports out of the box, giving it a lightweight and efficient memory footprint. This makes it the best choice for most iOS projects. Xcode’s Core Data template uses this store type by default.
  2. NSXMLStoreType is backed by an XML file, making it the most human-readable of all the store types. This store type is atomic, so it can have a large memory footprint. NSXMLStoreType is only available on OS X.
  3. NSBinaryStoreType is backed by a binary data file. Like NSXMLStoreType, it’s also an atomic store, so the entire binary file must be loaded onto memory before you can do anything with it. You’ll rarely find this type of persistent store in real- world applications.
  4. NSInMemoryStoreType is the in-memory persistent store type. In a way, this store type is not really persistent. Terminate the app or turn off your phone, and the data stored in an in-memory store type disappears into thin air. Although this may seem to defeat the purpose of Core Data, in-memory persistent stores can be helpful for unit testing and some types of caching.

The persistent store coordinator

NSPersistentStoreCoordinator is the bridge between the managed object model and the persistent store. It’s responsible for using the model and the persistent stores to do most of the hard work in Core Data. It understands the NSManagedObjectModel and knows how to send information to, and fetch information from, the NSPersistentStore.

NSPersistentStoreCoordinator also hides the implementation details of how your persistent store or stores are configured. This is useful for two reasons:

1. NSManagedObjectContext (coming next!) doesn’t have to know if it’s saving to an SQLite database, XML file or even a custom incremental store.

2. If you have multiple persistent stores, the persistent store coordinator presents a unified interface to the managed context. As far as the managed context is concerned, it always interacts with a single, aggregate persistent store.

The managed object context

  • A context is an in-memory scratchpad for working with your managed objects.
  • You do all of the work with your Core Data objects within a managed object context.
  • Any changes you make won’t affect the underlying data on disk until you call save() on the context.
  1. The context manages the lifecycle of the objects it creates or fetches. This lifecycle management includes powerful features such as faulting, inverse relationship handling and validation.
  2. A managed object cannot exist without an associated context. In fact, a managed object and its context are so tightly coupled that every managed object keeps a reference to its context, which can be accessed like so:
let managedContext = employee.managedObjectContext

Contexts are very territorial; once a managed object has been associated with a particular context, it will remain associated with the same context for the duration of its lifecycle.

  1. An application can use more than one context — most non-trivial Core Data applications fall into this category. Since a context is an in-memory scratch pad for what’s on disk, you can actually load the same Core Data object onto two different contexts simultaneously.
  2. A context is not thread-safe. The same goes for a managed object: You can only interact with contexts and managed objects on the same thread in which they were created.

The managed context is the only entry point required to access the rest of the stack. The persistent store coordinator is a public property on the NSManagedObjectContext. Similarly, both the managed object model and the array of persistent stores are public properties on the NSPersistentStoreCoordinator.

Core Data represents to-many relationships using sets, not arrays. Because you made the walks relationship ordered, you’ve got an NSOrderedSet.

NSOrderedSet is immutable, so you first have to create a mutable copy (NSMutableOrderedSet), insert the new walk and then reset an immutable copy of this mutable ordered set back on the dog.

Note: Deleting used to be one of the most “dangerous” Core Data operations. Why is this? When you remove something from Core Data, you have to delete both the record on disk as well as any outstanding references in code.

Trying to access an NSManagedObject that had no Core Data backing store resulted in the the much-feared inaccessible fault Core Data crash.

Starting with iOS 9, deletion is safer than ever. Apple introduced the property shouldDeleteInaccessibleFaults on NSManagedObjectContext, which is turned on by default. This marks bad faults as deleted and treats missing data as NULL/nil/0.

Core data Fetching

Different ways to write fetch requests.

// 1
let fetchRequest1 = NSFetchRequest<Venue>()
let entity =
NSEntityDescription.entity(forEntityName: "Venue",
in: managedContext)!
let fetchRequest2 = NSFetchRequest<Venue>(entityName: "Venue")
fetchRequest1.entity = entity
// 2 // 3
let fetchRequest3: NSFetchRequest<Venue> = Venue.fetchRequest()
// 4
let fetchRequest4 =
managedObjectModel.fetchRequestTemplate(forName: "venueFR")
// 5
let fetchRequest5 =
managedObjectModel.fetchRequestFromTemplate(
withName: "venueFR",
substitutionVariables: ["NAME" : "Vivi Bubble Tea"])

Note: If you’re not already familiar with it, NSFetchRequest is a generic type. If you inspect NSFetchRequest‘s initializer, you’ll notice it takes in type as a parameter <ResultType : NSFetchRequestResult>.

ResultType specifies the type of objects you expect as a result of the fetch request. For example, if you’re expecting an array of Venue objects, the result of the fetch request is now going to be [Venue] instead of [Any]. This is helpful because you don’t have to cast down to [Venue] anymore.

Here are all the possible values for a fetch request’s resultType:

.managedObjectResultType: Returns managed objects (default value).

.countResultType: Returns the count of the objects matching the fetch request.

.dictionaryResultType: This is a catch-all return type for returning the results of different calculations.

.managedObjectIDResultType: Returns unique identifiers instead of full-fledged managed objects.

Note: NSPredicate supports string-based key paths. This is why you can drill down from the Venue entity into the PriceInfo entity using priceInfo.priceCategory, and use the #keyPath keyword to get safe, compile-time checked values for the key path.As of this writing, NSPredicate does not support Swift 4 style key paths such as \Venue.priceInfo.priceCategory.

You’ve now used three of the four supported NSFetchRequest result types: .managedObjectResultType, .countResultType
and .dictionaryResultType.

The remaining result type is .managedObjectIDResultType. When you fetch with this type, the result is an array of NSManagedObjectID objects rather the actual managed objects they represent

An NSManagedObjectID is a compact universal identifier for a managed object. It works like the primary key in the database!

NSFetchRequest supports fetching batches. You can use the properties fetchBatchSize, fetchLimit and fetchOffset to control the batching behavior.

Core Data also tries to minimize its memory consumption for you by using a technique called faulting. A fault is a placeholder object representing a managed object that hasn’t yet been fully brought into memory.

You should also know that you can write predicates that check two conditions instead of one by using compound predicate operators such as AND, OR and NOT.

Alternatively, you can string two simple predicates into one compound predicate by using the class NSCompoundPredicate.

NSSortDescriptor. These sorts happen at the SQLite level, not in memory. This makes sorting in Core Data fast and efficient.

To initialize an instance of NSSortDescriptor you need three things: a key path to specify the attribute which you want to sort, a specification of whether the sort is ascending or descending and an optional selector to perform the comparison operation.

lazy var nameSortDescriptor: NSSortDescriptor = {
let compareSelector =
#selector(NSString.localizedStandardCompare(_:))
return NSSortDescriptor(key: #keyPath(Venue.name),
}()
ascending: true,
selector: compareSelector)
lazy var distanceSortDescriptor: NSSortDescriptor = {
return NSSortDescriptor(
key: #keyPath(Venue.location.distance),
ascending: true)
}()
lazy var priceSortDescriptor: NSSortDescriptor = {
return NSSortDescriptor(
key: #keyPath(Venue.priceInfo.priceCategory),
ascending: true)
}()

Any time you’re sorting user-facing strings, Apple recommends that you pass in NSString.localizedStandardCompare(_:) to sort according to the language rules of the current locale. This means sort will “just work” and do the right thing for languages with special characters. It’s the little things that matter, bien sûr!

Core Data has an API for performing long-running fetch requests in the background and getting a completion callback when the fetch completes.

var asyncFetchRequest: NSAsynchronousFetchRequest<Venue>?

The class responsible for this asynchronous magic is aptly called NSAsynchronousFetchRequest. Don’t be fooled by its name, though. It’s not directly related to NSFetchRequest; it’s actually a subclass of NSPersistentStoreRequest.

  1. Notice here that an asynchronous fetch request doesn’t replace the regular fetch request. Rather, you can think of an asynchronous fetch request as a wrapper around the fetch request you already had.
  2. To create an NSAsynchronousFetchRequest you need two things: a plain old NSFetchRequest and a completion handler. Your fetched venues are contained in NSAsynchronousFetchResult’s finalResult property. Within the completion handler, you update the venues property and reload the table view.
  3. Specifying the completion handler is not enough! You still have to execute the asynchronous fetch request. Once again, CoreDataStack’s managedContext property handles the heavy lifting for you. However, notice the method you use is different — this time, it’s execute(_:) instead of the usual fetch(_:).

execute(_:) returns immediately. You don’t need to do anything with the return value since you’re going to update the table view from within the completion block. The return type is NSAsynchronousFetchResult.

Note: As an added bonus to this API, you can cancel the fetch request with NSAsynchronousFetchResult’s cancel() method.

But what if you want to update a hundred thousand records all at once? It would take a lot of time and a lot of memory to fetch all of those objects just to update one attribute. No amount of tweaking your fetch request would save your user from having to stare at a spinner for a long, long time.

new way to update Core Data objects without having to fetch anything into memory: batch updates. This new technique greatly reduces the amount of time and memory required to make those huge kinds of updates.

The new technique bypasses the NSManagedObjectContext and goes straight to the persistent store. The classic use case for batch updates is the “Mark all as read” feature in a messaging application or e-mail client.

let batchUpdate = NSBatchUpdateRequest(entityName: "Venue")
batchUpdate.propertiesToUpdate =
[#keyPath(Venue.favorite): true]
batchUpdate.affectedStores =
coreDataStack.managedContext
.persistentStoreCoordinator?.persistentStores
batchUpdate.resultType = .updatedObjectsCountResultType
do {
let batchResult =
try coreDataStack.managedContext.execute(batchUpdate)
as? NSBatchUpdateResult
print("Records updated \(String(describing:
batchResult?.result))")
} catch let error as NSError {
print("Could not update \(error), \(error.userInfo)")
}

NSBatchDeleteRequest for this purpose.

As the name suggests, a batch delete request can efficiently delete a large number Core Data objects in one go.

Like NSBatchUpdateRequest, NSBatchDeleteRequest is also a subclass of NSPersistentStoreRequest. Both types of batch request behave similarly since they both operate directly on the persistent store.

Note: Since you’re sidestepping your NSManagedObjectContext, you won’t get any validation if you use a batch update request or a batch delete request. Your changes also won’t be reflected in your managed context.

Make sure you’re sanitizing and validating your data properly before using a persistent store request!

  • Use NSFetchRequest’s count result type to efficiently compute and return counts from SQLite.
  • Use NSFetchRequest’s dictionary result type to efficiently compute and return averages, sums and other common calculations from SQLite.
  • A fetch request uses different techniques such as using batch sizes, batch limits and faulting to limit the amount of information returned.
  • Add a sort description to your fetch request to efficiently sort your fetched results.

NSFetchedResultsController = connection between UITableView and Core Data

lazy var fetchedResultsController:
NSFetchedResultsController<Team> = {
// 1
let fetchRequest: NSFetchRequest<Team> = Team.fetchRequest()
// 2
let fetchedResultsController = NSFetchedResultsController(
fetchRequest: fetchRequest,
managedObjectContext: coreDataStack.managedContext,
sectionNameKeyPath: nil,
cacheName: nil)
return fetchedResultsController
}()
  1. The fetched results controller handles the coordination between Core Data and your table view, but it still needs you to provide an NSFetchRequest. Remember the NSFetchRequest class is highly customizable. It can take sort descriptors, predicates, etc.
  2. In this example, you get your NSFetchRequest directly from the Team class because you want to fetch all Team objects.
  3. The initializer method for a fetched results controller takes four parameters: first up, the fetch request you just created.
  4. The second parameter is an instance of NSManagedObjectContext. Like NSFetchRequest, the fetched results controller class needs a managed object context to execute the fetch. It can’t actually fetch anything by itself.
  5. The other two parameters are optional: sectionNameKeyPath and cacheName.
  6. sectionNameKeyPath parameter. You can use this parameter to specify an attribute the fetched results controller should use to group the results and generate sections.
  7. Note: sectionNameKeyPath takes a keyPath string. It can take the form of an attribute name such as qualifyingZone or teamName, or it can drill deep into a Core Data relationship, such as employee.address.street. Use the #keyPath syntax to defend against typos and stringly typed code.

NSFetchedResultsController is both a wrapper around a fetch request and a container for its fetched results. You can get them either with the fetchedObjects property or the object(at:) method.

Note: The sections array contains opaque objects that implement the NSFetchedResultsSectionInfo protocol. This lightweight protocol provides information about a section, such as its title and number of objects.

'An instance of NSFetchedResultsController requires a fetch
request with sort descriptors'

Its minimum requirement is you set an entity description, and it will fetch all objects of that entity type. NSFetchedResultsController, however, requires at least one sort descriptor

let zoneSort = NSSortDescriptor(
key: #keyPath(Team.qualifyingZone),
ascending: true)
let scoreSort = NSSortDescriptor(
key: #keyPath(Team.wins),
ascending: false)
let nameSort = NSSortDescriptor(
key: #keyPath(Team.teamName),
ascending: true)
fetchRequest.sortDescriptors = [zoneSort, scoreSort, nameSort]

NSFetchedResultsController thought about this problem and came up with a solution: caching.

let fetchedResultsController = NSFetchedResultsController(
fetchRequest: fetchRequest,
managedObjectContext: coreDataStack.managedContext,
sectionNameKeyPath: #keyPath(Team.qualifyingZone),
cacheName: "worldCup")

You specify a cache name to turn on NSFetchedResultsController’s on-disk section cache. That’s all you need to do! Keep in mind that this section cache is completely separate from Core Data’s persistent store, where you persist the teams.

Note: NSFetchedResultsController’s section cache is very sensitive to changes in its fetch request. As you can imagine, any changes — such as a different entity description or different sort descriptors — would give you a completely different set of fetched objects, invalidating the cache completely. If you make changes like this, you must delete the existing cache using deleteCache(withName:) or use a different cache name.

Three main benefits of using NSFetchedResultsController: sections and caching, listen for changes in the result set

NSFetchedResultsController can listen for changes in its result set and notify its delegate, NSFetchedResultsControllerDelegate. You can use this delegate to refresh the table view as needed any time the underlying data changes.

Note: A fetched results controller can only monitor changes made via the managed object context specified in its initializer. If you create a separate NSManagedObjectContext somewhere else in your app and start making changes there, your delegate method won’t run until those changes have been saved and merged with the fetched results controller’s context.

  • controllerWillChangeContent(_:): This delegate method notifies you that changes are about to occur. You ready your table view using beginUpdates().
  • controller(_:didChange:at:for:newIndexPath:): This method is quite a mouthful. And with good reason — it tells you exactly which objects changed, what type of change occurred (insertion, deletion, update or reordering) and what the affected index paths are.

This middle method is the proverbial glue that synchronizes your table view with Core Data. No matter how much the underlying data changes, your table view will stay true to what’s going on in the persistent store.

  • controllerDidChangeContent(_:): The delegate method you had originally implemented to refresh the UI turned out to be the third of three delegate methods that notify you of changes. Rather than refreshing the entire table view, you just need to call endUpdates() to apply the changes
  • NSFetchedResultsController abstracts away most of the code needed to synchronize a table view with a Core Data store.
  • At its core, NSFetchedResultsController is a wrapper around an NSFetchRequest and a container for its fetched results.
  • A fetched results controller requires setting at least one sort descriptor on its fetch request. If you forget the sort descriptor, your app will crash.
  • You can set a fetched result’s controller sectionNameKeyPath to specify an attribute to group the results into table view sections. Each unique value corresponds to a different table view section.
  • Grouping a set of fetched results into sections is an expensive operation. Avoid having to compute sections multiple times by specifying a cache name on your fetched results controller.
  • A fetched results controller can listen for changes in its result set and notify its delegate, NSFetchedResultsControllerDelegate, to respond to these changes.
  • NSFetchedResultsControllerDelegate monitors changes in individual Core Data records (whether they were inserted, deleted or modified) as well as changes to entire sections.
  • Diffable data sources make working with fetched results controllers and table views easier.

Migration

When is a migration necessary? The easiest answer to this common question is “when you need to make changes to the data model.”

If an app is using Core Data merely as an offline cache, when you update the app, you can simply delete and rebuild the data store. This is only possible if the source of truth for your user’s data isn’t in the data store. In all other cases, you’ll need to safeguard your user’s data.

First, Core Data analyzes the store’s model version. Next, it compares this version to the coordinator’s configured data model. If the store’s model version and the coordinator’s model version don’t match, Core Data will perform a migration, when enabled.

Note: If migrations aren’t enabled, and the store is incompatible with the model, Core Data will simply not attach the store to the coordinator and specify an error with an appropriate reason code.

To start the migration process, Core Data needs the original data model and the destination model. It uses these two versions to load or create a mapping model for the migration, which it uses to convert data in the original store to data that it can store in the new store. Once Core Data determines the mapping model, the migration process can start in earnest.

  1. First, Core Data copies over all the objects from one data store to the next.
  2. Next, Core Data connects and relates all the objects according to the relationship mapping.
  3. Finally, enforce any data validations in the destination model. Core Data disables destination model validations during the data copy.

Lightweight migrations

Lightweight migration is Apple’s term for the migration with the least amount of work involved on your part. This happens automatically when you use NSPersistentContainer, or you have to set some flags when building your own Core Data stack. There are some limitations on how much you can change the data model, but because of the small amount of work required to enable this option, it’s the ideal setting.

This is level 3 on the migration complexity index. You’ll still use a mapping model, but complement that with custom code to specify custom transformation logic on data. Custom entity transformation logic involves creating an NSEntityMigrationPolicy subclass and performing custom transformations there.

lightweight migrations are enabled by default. This means every time you create a new data model version, and it can be auto migrated, it will be.

It just so happens Core Data can infer a mapping model in many cases when you enable the shouldInferMappingModelAutomatically flag on the NSPersistentStoreDescription. Core Data can automatically look at the differences in two data models and create a mapping model between them.

For entities and attributes that are identical between model versions, this is a straightforward data pass through mapping. For other changes, just follow a few simple rules for Core Data to create a mapping model.

In the new model, changes must fit an obvious migration pattern, such as:

  • Deleting entities, attributes or relationships
  • Renaming entities, attributes or relationships using the renamingIdentifier
  • Adding a new, optional attribute
  • Adding a new, required attribute with a default value
  • Changing an optional attribute to non-optional and specifying a default value
  • Changing a non-optional attribute to optional
  • Changing the entity hierarchy
  • Adding a new parent entity and moving attributes up or down the hierarchy
  • Changing a relationship from to-one to to-many
  • Changing a relationship from non-ordered to-many to ordered to-many (and vice versa)

During the process for creating a new Mapping Model, you’ll essentially lock in the source and destination model versions into the Mapping Model file.

This means any changes you make to the actual data model after creating the mapping model will not be seen by the Mapping Model.

description.shouldMigrateStoreAutomatically = true
description.shouldInferMappingModelAutomatically = false

This should generate a value expression that looks like this:

FUNCTION($manager,
"destinationInstancesForEntityMappingNamed:sourceInstances:",
"NoteToNote", $source)

The FUNCTION statement resembles the objc_msgSend syntax; that is, the first argument is the object instance, the second argument is the selector and any further arguments are passed into that method as parameters.

  1. First, you create an instance of the new destination object. The migration manager has two Core Data stacks — one to read from the source and one to write to the destination — so you need to be sure to use the destination context here. Now, you might notice that this section isn’t using the new fancy short ImageAttachment(context: NSManagedObjectContext) initializer. Well, as it turns out, this migration will simply crash using the new syntax, because it depends on the model having been loaded and finalized, which hasn’t happened halfway through a migration.
  2. Next, create a traversePropertyMappings function that performs the task of iterating over the property mappings if they are present in the migration. This function will control the traversal while the next section will perform the operation required for each property mapping.
  3. If, for some reason, the attributeMappings property on the entityMapping object doesn’t return any mappings, this means your mappings file has been specified incorrectly. When this happens, the method will throw an error with some helpful information.
  4. Even though it’s a custom manual migration, most of the attribute migrations should be performed using the expressions you defined in the mapping model. To do this, use the traversal function from the previous step and apply the value expression to the source instance and set the result to the new destination object.
  5. Next, try to get an instance of the image. If it exists, grab its width and height to populate the data in the new object.
  6. For the caption, simply grab the note’s body text and take the first 80 characters.
  7. The migration manager needs to know the connection between the source object, the newly created destination object and the mapping. Failing to call this method at the end of a custom migration will result in missing data in the destination store.

Core data Optimisations

Binary data attributes are usually stored right in the database. If you check the Allows External Storage option, Core Data automatically decides if it’s better to save the data to disk as a separate file or leave it in the SQLite database.

Core Data fetch requests include the fetchBatchSize property, which makes it easy to fetch just enough data, but not too much.

If you don’t set a batch size, Core Data uses the default value of 0, which disables batching.

Setting a non-zero positive batch size lets you limit the amount of data returned to the batch size. As the app needs more data, Core Data automatically performs more batch operations

A good rule of thumb is to set the batch size to about double the number of items that appear onscreen at any given time. The employee list shows three to five employees onscreen at once, so 10 is a reasonable batch size.

Fetch requests use predicates to limit the amount of data returned. As mentioned above, for optimal performance, you should limit the amount of data you fetch to the minimum needed: the more data you fetch, the longer the fetch will take.

You can limit your fetch requests by using predicates. If your fetch request requires a compound predicate, you can make it more efficient by putting the more restrictive predicate first. This is especially true if your predicate contains string comparisons. For example, a predicate with a format of “(active == YES) AND (name CONTAINS[cd] %@)” would likely be more efficient than “(name CONTAINS[cd] %@) AND (active == YES)”.

func testTotalEmployeesPerDepartment() {
measureMetrics(
[.wallClockTime],
automaticallyStartMeasuring: false ){
let departmentList = DepartmentListViewController()
departmentList.coreDataStack =
CoreDataStack(modelName: "EmployeeDirectory")
startMeasuring()
_ = departmentList.totalEmployeesPerDepartment()
stopMeasuring()
} }

This function uses measureMetrics to see how long code takes to execute.

Note: NSExpression is a powerful API, yet it is seldom used, at least directly. When you create predicates with comparison operations, you may not know it, but you’re actually using expressions. There are many pre-built statistical and arithmetical expressions available in NSExpression, including average, sum, count, min, max, median, mode and stddev.

Calling fatalError, at the very least, generates a stack trace, which can be helpful when trying to fix the problem. If your app has support for remote logging or crash reporting, you should log any relevant information that might be helpful for debugging before calling fatalError.

NSPersistentCloudKit

CloudKit solves that problem. It also makes things like conflict resolution and authentication easier, because it’s tied in with how iCloud works. Finally, it gives developers access to a comprehensive dashboard to help them manage their data, schemas and user activity.

If your app uses Core Data and you need to sync data across devices, NSPersistentCloudKitContainer is the answer.

CloudKit-backed Core Data models have some specific requirements. For example, they don’t support unique constraints, undefined and objectID attributes or deny deletion rules.

Any relationships between entities must have an inverse relationship. Also, Model Configurations must not have entities related to entities in other configurations.

If you have a model with any of these unsupported features, solve the problem by creating a new, separate model. Use that new model to store the specific data you want to sync.

During the development process, you upload the latest schema to CloudKit as you make changes to the app. When you’re ready to release your app, you promote the final schema to production status. Once a schema is in production, you can never rename CloudKit record names or types, but you can add fields and entities

NSPersistentCloudKitContainer heavily relies upon push notifications to notify devices when data changes. While the simulator works for development, it won’t respond in real-time to changes made in another simulator. You’ll need a real device to test with and a paid developer account with Apple to configure push notifications.

  • NSPersistentCloudKitContainer is a powerful addition to Core Data to power multi-device sync for your app.
  • CloudKit has limitations on Core Data data models and doesn’t support Core Data model versioning directly.
  • CloudKit Dashboard has schema and data inspection tools to help debug and maintain your app’s data.
  • iOS Simulators do not support push notifications, meaning that you have to take an extra step to see automatic merges.
  • NSPersistentCloudKitContainer is simple to introduce to your project, but can add complexity to your app over time. Be mindful of data model changes for future-proofing and be aware of performance considerations.

Resource — CoreData by Tutorials (By the raywenderlich Tutorial Team)

--

--

Aman Kesarwani

iOS Application/Framework Developer| Mobile Architecture | SwiftUI | Swift, Objective C |