Multiple Managed Object Contexts with Core Data Tutorial

A managed object context is an in-memory scratchpad for working with your managed objects.

Most apps need just a single managed object context. The default configuration in most Core Data apps is a single managed object context associated with the main queue. Multiple managed object contexts make your apps harder to debug; it’s not something you’d use in every app, in every situation.

That being said, certain situations do warrant the use of more than one managed object context. For example, long-running tasks, such as exporting data, will block the main thread of apps that use only a single main-queue managed object context and cause the UI to stutter.

In other situations, such as when edits are being made to user data, it’s helpful to treat a managed object context as a set of changes that the app can discard if it no longer needs them. Using child contexts makes this possible.

In this tutorial, you’ll learn about multiple managed object contexts by taking a journaling app for surfers and improving it in several ways by adding multiple contexts.

Note: This is an advanced tutorial, and assumes prior knowledge of Swift, Core Data, and iOS app development in general. If common Core Data phrases such as managed object subclass and persistent store coordinator don’t ring any bells, or if you’re unsure what a Core Data stack is supposed to do, you may want to read some of our other Core Data tutorials first.

Getting Started

This tutorial’s starter project is a simple journal app for surfers. After each surf session, a surfer can use the app to create a new journal entry that records marine parameters, such as swell height or period, and rate the session from 1 to 5. Dude, if you’re not fond of hanging ten and getting barreled, no worries, brah. Just replace the surfing terminology with your favorite hobby of choice!

Introducing SurfJournal

Download the SurfJournal starter project here. Open the project, then build and run the app.

On startup, the application lists all previous surf session journal entries. Tapping a row brings up the detail view of a surf session with the ability to make edits.

surf_journal_screenshots-386x320
As you can see, the sample app works and has data. Tapping the Export button on the top-left exports the data to a comma-separated values (CSV) file. Tapping the plus (+) button on the top-right adds a new journal entry. Tapping a row in the list opens the entry in edit mode, where you can change or view the details of a surf session.

Although the sample project appears simple, it actually does a lot and will serve as a good base to add multi-context support. First, let’s make sure you have a good understanding of the various classes in the project.

Open the project navigator and take a look at the full list of files in the starter project:

Before jumping into the code, take a brief moment to go over what each class does for you out of the box.

  • AppDelegate: On first launch, the app delegate creates the Core Data stack and sets the coreDataStack property on the primary view controller JournalListViewController.
  • CoreDataStack: This object contains the cadre of Core Data objects known as the stack. Here, the stack installs a database that already has data in it on first launch. No need to worry about this just yet; you’ll see how it works shortly.
  • JournalListViewController: The sample project is a one-page, table-based application. This file represents that table. If you’re curious about its UI elements, head over to Main.storyboard. There’s a table view controller embedded in a navigation controller and a single prototype cell of type SurfEntryTableViewCell.
  • JournalEntryViewController: This class handles creating and editing surf journal entries. You can see its UI in Main.storyboard.
  • JournalEntry: This class represents a surf journal entry. It’s an NSManagedObject subclass with six properties for attributes: date, height, location, period, rating and wind. If you’re curious about this class’s entity definition, check out SurfJournalModel.xcdatamodel.

image4-2-480x157

  • JournalEntry+Helper: This is an extension to the JournalEntry object. It includes the CSV export method csv() and the stringForDate() helper method. These methods are implemented in the extension to avoid being destroyed when you make changes to the Core Data model.

There was already a significant amount of data when you first launched the app.This sample project comes with a seeded Core Data database.

The Core Data Stack

Open CoreDataStack.swift and find the following code in seedCoreDataContainerIfFirstLaunch():

// 1
let previouslyLaunched =
  UserDefaults.standard.bool(forKey: "previouslyLaunched")
if !previouslyLaunched {
  UserDefaults.standard.set(true, forKey: "previouslyLaunched")

  // Default directory where the CoreDataStack will store its files
  let directory = NSPersistentContainer.defaultDirectoryURL()
  let url = directory.appendingPathComponent(
    modelName + ".sqlite")

  // 2: Copying the SQLite file
  let seededDatabaseURL = Bundle.main.url(
    forResource: modelName,
    withExtension: "sqlite")!

  _ = try? FileManager.default.removeItem(at: url)

  do {
    try FileManager.default.copyItem(at: seededDatabaseURL,
                                     to: url)
  } catch let nserror as NSError {
    fatalError("Error: \(nserror.localizedDescription)")
  }  

Here’s what this version of CoreDataStack.swift contains:

  1. You first check UserDefaults for the previouslyLaunched boolean value. If the current execution is indeed the app’s first launch, the Bool will be false, making the if statement true. On first launch, the first thing you do is set previouslyLaunched to true so the seeding operation never happens again.
  2. You then copy the SQLite seed file SurfJournalModel.sqlite, included with the app bundle, to the directory returned by the Core Data-provided method NSPersistentContainer.defaultDirectoryURL().

Now view the rest of seedCoreDataContainerIfFirstLaunch():

  // 3: Copying the SHM file
  let seededSHMURL = Bundle.main.url(forResource: modelName,
    withExtension: "sqlite-shm")!
  let shmURL = directory.appendingPathComponent(
    modelName + ".sqlite-shm")

  _ = try? FileManager.default.removeItem(at: shmURL)

  do {
    try FileManager.default.copyItem(at: seededSHMURL,
                                     to: shmURL)
  } catch let nserror as NSError {
    fatalError("Error: \(nserror.localizedDescription)")
  }

  // 4: Copying the WAL file
  let seededWALURL = Bundle.main.url(forResource: modelName,
    withExtension: "sqlite-wal")!
  let walURL = directory.appendingPathComponent(
    modelName + ".sqlite-wal")

  _ = try? FileManager.default.removeItem(at: walURL)

  do {
    try FileManager.default.copyItem(at: seededWALURL,
                                     to: walURL)
  } catch let nserror as NSError {
    fatalError("Error: \(nserror.localizedDescription)")
  }

  print("Seeded Core Data")
}
  1. Once the copy of SurfJournalModel.sqlite has succeeded, you then copy over the support file SurfJournalModel.sqlite-shm.
  2. Finally, you copy over the remaining support file SurfJournalModel.sqlite-wal.

The only reason SurfJournalModel.sqlite, SurfJournalModel.sqlite-shm or SurfJournalModel.sqlite-wal would fail to copy on first launch is if something really bad happened, such as disk corruption from cosmic radiation. In that case, the device, including any apps, would likely also fail. If the files fail to copy, there’s no point in continuing, so the catch blocks call fatalError.

Note: Developers often frown upon using abort and fatalError, as it confuses users by causing the app to quit suddenly and without explanation. This is one scenario where fatalError is acceptable, since the app needs Core Data to work. If an app requires Core Data and Core Data isn’t working, there’s no point in letting the app continue on, only to fail sometime later in a non-deterministic way.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.

To support concurrent reads and writes, the persistent SQLite store in this sample app uses SHM (shared memory file) and WAL (write-ahead logging) files. You don’t need to know how these extra files work, but you do need to be aware of their existence, and that you need to copy them over when seeding the database. If you fail to copy over these files, the app will work, but it might be missing data.

Now that you know something about beginning with a seeded database, you’ll start learning about multiple managed object contexts by working on a temporary private context.

Doing Work In the Background

If you haven’t done so already, tap the Export button at the top-left and then immediately try to scroll the list of surf session journal entries. Notice anything? The export operation takes several seconds, and it prevents the UI from responding to touch events such as scrolling.

The UI is blocked during the export operation because both the export operation and UI are using the main queue to perform their work. This is the default behavior.

The traditional way to fix this is to use Grand Central Dispatch to run the export operation on a background queue. However, Core Data managed object contexts are not thread-safe. That means you can’t just dispatch to a background queue and use the same Core Data stack.

The solution is simple: use a private background queue rather than the main queue for the export operation. This will keep the main queue free for the UI to use.

But before you jump in and fix the problem, you need to understand how the export operation works.

Exporting Data

Start by viewing how the app creates the CSV strings for the JournalEntry entity. Open JournalEntry+Helper.swift and find csv():

func csv() -> String {
  let coalescedHeight = height ?? ""
  let coalescedPeriod = period ?? ""
  let coalescedWind = wind ?? ""
  let coalescedLocation = location ?? ""
  let coalescedRating: String
  if let rating = rating?.int32Value {
    coalescedRating = String(rating)
  } else {
    coalescedRating = ""
  }

  return "\(stringForDate()),\(coalescedHeight),\(coalescedPeriod),\(coalescedWind),\(coalescedLocation),\(coalescedRating)\n"
}

As you can see, JournalEntry returns a comma-separated string of the entity’s attributes. Because the JournalEntry attributes are allowed to be nil, the function uses the nil coalescing operator (??) to export an empty string instead of an unhelpful debug message that the attribute is nil.

Note: The nil coalescing operator (??) unwraps an optional if it contains a value; otherwise it returns a default value. For example, the following: let coalescedHeight = height != nil ? height! : "" can be shortened using the nil coalescing operator to: let coalescedHeight = height ?? "".

That’s how the app creates the CSV strings for an individual journal entry, but how does the app save the CSV file to disk? Open JournalListViewController.swift and find the following code in exportCSVFile():

// 1
let context = coreDataStack.mainContext
var results: [JournalEntry] = []
do {
  results = try context.fetch(self.surfJournalFetchRequest())
} catch let error as NSError {
  print("ERROR: \(error.localizedDescription)")
}

// 2
let exportFilePath = NSTemporaryDirectory() + "export.csv"
let exportFileURL = URL(fileURLWithPath: exportFilePath)
FileManager.default.createFile(atPath: exportFilePath,
  contents: Data(), attributes: nil)

Going through the CSV export code step-by-step:

  1. First, retrieve all JournalEntry entities by executing a fetch request.The fetch request is the same one used by the fetched results controller. Therefore, you reuse the surfJournalFetchRequest method to create the request to avoid duplication.
  2. Next, create the URL for the exported CSV file by appending the file name (“export.csv”) to the output of the NSTemporaryDirectory method.The path returned by NSTemporaryDirectory is a unique directory for temporary file storage. This a good place for files that can easily be generated again and don’t need to be backed up by iTunes or to iCloud.

    After creating the export URL, call createFile(atPath:contents:attributes:) to create the empty file where you’ll store the exported data. If a file already exists at the specified file path, this method will remove it first.

Once the app has the empty file, it can write the CSV data to disk:

// 3
let fileHandle: FileHandle?
do {
  fileHandle = try FileHandle(forWritingTo: exportFileURL)
} catch let error as NSError {
  print("ERROR: \(error.localizedDescription)")
  fileHandle = nil
}

if let fileHandle = fileHandle {
  // 4
  for journalEntry in results {
    fileHandle.seekToEndOfFile()
    guard let csvData = journalEntry
      .csv()
      .data(using: .utf8, allowLossyConversion: false) else {
        continue
    }

    fileHandle.write(csvData)
  }

  // 5
  fileHandle.closeFile()

  print("Export Path: \(exportFilePath)")
  self.navigationItem.leftBarButtonItem = 
    self.exportBarButtonItem()
  self.showExportFinishedAlertView(exportFilePath)

} else {
  self.navigationItem.leftBarButtonItem = 
    self.exportBarButtonItem()
}

Here’s how the file-handling works:

  1. First, the app needs to create a file handler for writing, which is simply an object that handles the low-level disk operations necessary for writing data. To create a file handler for writing, use the FileHandle(forWritingTo:) initializer.
  1. Next, iterate over all JournalEntry entities.During each iteration, you attempt to create a UTF8-encoded string using csv() on JournalEntry and data(using:allowLossyConversion:) on String.

    If it’s successful, you write the UTF8 string to disk using the file handler write() method.

  2. Finally, close the export file-writing file handler, since it’s no longer needed.

Once the app has written all the data to disk, it shows an alert dialog with the exported file path.

Note: This alert controller with the export path is fine for learning purposes, but for a real app, you’ll need to provide the user with a way to retrieve the exported CSV file, for example using UIActivityViewController.

To open the exported CSV file, use Excel, Numbers or your favorite text editor to navigate to and open the file specified in the alert dialog. If you open the file in Numbers you will see the following:

Now that you’ve seen how the app currently exports data, it’s time to make some improvements.

Exporting In the Background

You want the UI to continue working while the export is happening. To fix the UI problem, you’ll perform the export operation on a private background context instead of on the main context.

Open JournalListViewController.swift and find the following code in exportCSVFile():

// 1
let context = coreDataStack.mainContext
var results: [JournalEntry] = []
do {
  results = try context.fetch(self.surfJournalFetchRequest())
} catch let error as NSError {
  print("ERROR: \(error.localizedDescription)")
}

As you saw earlier, this code retrieves all of the journal entries by calling fetch() on the managed object context.

Next, replace the above code with the following:

// 1
coreDataStack.storeContainer.performBackgroundTask { context in
  var results: [JournalEntry] = []
  do {
    results = try context.fetch(self.surfJournalFetchRequest())
  } catch let error as NSError {
    print("ERROR: \(error.localizedDescription)")
  }

Instead of using the main managed object context also used by the UI, you’re now calling performBackgroundTask(_:) method. This creates and executes the code block on that private context.

The private context created by performBackgroundTask(_:) is on a private queue, which doesn’t block the main UI queue. You could also manually create a new temporary private context with a concurrency type of .privateQueueConcurrencyType instead of using performBackgroundTask(_:).

Note: There are two concurrency types a managed object context can use:Private Queue specifies the context that will be associated with a private dispatch queue instead of the main queue. This is the type of queue you just used to move the export operation off of the main queue so it would no longer interfere with the UI.

Main Queue, the default type, specifies that the context will be associated with the main queue. This type is what the main context (coreDataStack.mainContext) uses. Any UI operation, such as creating the fetched results controller for the table view, must use a context of this type.

Next, find the following code in the same method:

  print("Export Path: \(exportFilePath)")
  self.navigationItem.leftBarButtonItem = 
    self.exportBarButtonItem()
  self.showExportFinishedAlertView(exportFilePath)
} else {
  self.navigationItem.leftBarButtonItem = 
    self.exportBarButtonItem()
}

Replace the code with the following:

    print("Export Path: \(exportFilePath)")
    // 6
    DispatchQueue.main.async {
      self.navigationItem.leftBarButtonItem =
        self.exportBarButtonItem()
      self.showExportFinishedAlertView(exportFilePath)
    }
  } else {
    DispatchQueue.main.async {
      self.navigationItem.leftBarButtonItem = 
        self.exportBarButtonItem()
    }
  }
} // 7 Closing brace for performBackgroundTask

To finish off the task:

  1. You should always perform all operations related to the UI on the main queue, such as showing an alert view when the export operation is finished; otherwise, unpredictable things might happen. Use DispatchQueue.main.async to show the final alert view message on the main queue.
  2. Finally, add a closing curly brace to close the block you opened earlier in step 1 via the performBackgroundTask(_:) call.

surf

Now that you’ve moved the export operation to a new context with a private queue, build and run to see if it works!

You should see exactly what you saw before:

Tap the Export button in the top left, and immediately try to scroll the list of surf session journal entries. Notice anything different this time? The export operation still takes several seconds to complete, but the table view continues to scroll during this time. The export operation is no longer blocking the UI.

Cowabunga, dude! Gnarly job making the UI more responsive.

You’ve just witnessed how doing work on a private background queue can improve a user’s experience with your app. Now you’ll expand on the use of multiple contexts by examining a child context.

Editing On a Scratchpad

Right now, SurfJournal uses the main context (coreDataStack.mainContext) when creating a new journal entry or viewing an existing one. There’s nothing wrong with this approach; the starter project works as-is.

For journaling-style apps like this one, you can simplify the app architecture by thinking of edits or new entries as a set of changes, like a scratch pad. As the user edits the journal entry, you update the attributes of the managed object. Once the changes are complete, you either save them or throw them away, depending on what the user wants to do.

You can think of child managed object contexts as temporary scratch pads that you can either discard completely, or save and send the changes to the parent context.

But what is a child context, technically?

All managed object contexts have a parent store from which you can retrieve and change data in the form of managed objects, such as the JournalEntry objects in this project. Typically, the parent store is a persistent store coordinator, which is the case for the main context provided by the CoreDataStack class. Alternatively, you can set the parent store for a given context to another managed object context, making it a child context.

image7-2-358x320

When you save a child context, the changes only go to the parent context. Changes to the parent context won’t be sent to the persistent store coordinator until the parent context is saved.

Before you jump in and add a child context, you need to understand how the current viewing and editing operation works.

Viewing and Editing

The first part of the operation requires segueing from the main list view to the journal detail view. Open JournalListViewController.swift and find prepare(for:sender:):

// 1
if segue.identifier == "SegueListToDetail" {
  // 2
  guard let navigationController =
    segue.destination as? UINavigationController,
    let detailViewController =
      navigationController.topViewController
        as? JournalEntryViewController,
    let indexPath = tableView.indexPathForSelectedRow else {
      fatalError("Application storyboard mis-configuration")
  }
  // 3
  let surfJournalEntry = 
    fetchedResultsController.object(at: indexPath)
  // 4
  detailViewController.journalEntry = surfJournalEntry
  detailViewController.context = 
    surfJournalEntry.managedObjectContext
  detailViewController.delegate = self

Taking the segue code step-by-step:

  1. There’s two segues: SegueListToDetail and SegueListToDetailAdd. The first, shown in the previous code block, runs when the user taps on a row in the table view to view or edit a previous journal entry.
  2. Next, you get a reference to the JournalEntryViewController the user is going to end up seeing. It’s presented inside a navigation controller so there’s some unpacking to do. This code also verifies that there’s a selected index path in the table view.
  3. Next, you get the JournalEntry selected by the user, using the fetched results controller’s object(at:)method.
  4. Finally, you set all required variables on the JournalEntryViewController instance. The surfJournalEntry variable corresponds to the JournalEntry entity resolved in step 3. The context variable is the managed object context to be used for any operation; for now, it just uses the main context. The JournalListViewController sets itself as the delegate of the JournalEntryViewControllerso it can be informed when the user has completed the edit operation.

SegueListToDetailAdd is similar to SegueListToDetail, except the app creates a new JournalEntry entity instead of retrieving an existing one. The app executes SegueListToDetailAdd when the user taps the plus (+) button on the top-right to create a new journal entry.

Now that you know how both segues work, switch to JournalEntryViewController.swift and look at the JournalEntryDelegate protocol at the top of the file:

protocol JournalEntryDelegate {
  func didFinish(viewController: JournalEntryViewController,
                 didSave: Bool)
}

The JournalEntryDelegate protocol is very short and consists of only one method: didFinish(viewController:didSave:). This method, which the protocol requires the delegate to implement, indicates if the user is done editing or viewing a journal entry and whether any changes should be saved.

To understand how didFinish(viewController:didSave:) works, switch back to JournalListViewController.swift and find that method:

func didFinish(viewController: JournalEntryViewController,
               didSave: Bool) {
  // 1
  guard didSave,
    let context = viewController.context,
    context.hasChanges else {
      dismiss(animated: true)
      return
  }
  // 2
  context.perform {
    do {
      try context.save()
    } catch let error as NSError {
      fatalError("Error: \(error.localizedDescription)")
    }
    // 3
    self.coreDataStack.saveContext()
  }
  // 4
  dismiss(animated: true)
}

Taking each numbered comment in turn:

  1. First, use a guard statement to check the didSave parameter. This will be true if the user taps the Save button instead of the Cancel button, so the app should save the user’s data. The guard statement also uses the hasChanges property to check if anything’s changed; if nothing has changed, there’s no need to waste time doing more work.
  2. Next, save the JournalEntryViewController context inside of a perform(_:) closure. The code sets this context to the main context; in this case it’s a bit redundant since there’s only one context, but this doesn’t change the behavior.Once you add a child context to the workflow later on, the JournalEntryViewController context will be different from the main context, making this code necessary.

    If the save fails, call fatalError to abort the app with the relevant error information.

  3. Next, save the main context via saveContext, defined in CoreDataStack.swift, persisting any edits to disk.
  4. Finally, dismiss the JournalEntryViewController.
Note: If a managed object context is of type MainQueueConcurrencyType, you don’t have to wrap code in perform(_:), but it doesn’t hurt to use it.If you don’t know what type the context will be, as is the case in didFinish(viewController:didSave:), it’s safest to use perform(_:) so it will work with both parent and child contexts.

There’s a problem with the above implementation — have you spotted it?

When the app adds a new journal entry, it creates a new object and adds it to the managed object context. If the user taps the Cancel button, the app won’t save the context, but the new object will still be present. If the user then adds and saves another entry, the canceled object will still be present! You won’t see it in the UI unless you’ve got the patience to scroll all the way to the end, but it will show up at the bottom of the CSV export.

You could solve this problem by deleting the object when the user cancels the view controller. But what if the changes were complex, involved multiple objects, or required you to alter properties of an object as part of the editing workflow? Using a child context will help you manage these complex situations with ease.

Using Child Contexts for Sets of Edits

Now that you know how the app currently edits and creates JournalEntry entities, you’ll modify the implementation to use a child managed object context as a temporary scratch pad.

It’s easy to do — you simply need to modify the segues. Open JournalListViewController.swift and find the following code for SegueListToDetail in prepare(for:sender:):

detailViewController.journalEntry = surfJournalEntry
detailViewController.context =
  surfJournalEntry.managedObjectContext
detailViewController.delegate = self

Next, replace that code with the following:

// 1
let childContext = NSManagedObjectContext(
  concurrencyType: .mainQueueConcurrencyType)
childContext.parent = coreDataStack.mainContext

// 2
let childEntry = childContext.object(
  with: surfJournalEntry.objectID) as? JournalEntry

// 3
detailViewController.journalEntry = childEntry
detailViewController.context = childContext
detailViewController.delegate = self

Here’s the play-by-play:

  1. First, you create a new managed object context named childContext with a .mainQueueConcurrencyType. Here you set a parent context instead of a persistent store coordinator as you would normally do when creating a managed object context. Here, you set parent to mainContextof your CoreDataStack.
  2. Next, you retrieve the relevant journal entry using the child context’s object(with:) method. You must use object(with:) to retrieve the journal entry because managed objects are specific to the context that created them. However, objectID values are not specific to a single context, so you can use them when you need to access objects in multiple contexts.
  3. Finally, you set all required variables on the JournalEntryViewController instance. This time, you use childEntry and childContext instead of the original surfJournalEntry and surfJournalEntry.managedObjectContext.
Note: You might be wondering why you need to pass both the managed object and the managed object context to the detailViewController, since managed objects already have a context variable. This is because managed objects only have a weak reference to the context. If you don’t pass the context, ARC will remove the context from memory (since nothing else is retaining it) and the app will not behave as you expect.

Build and run your app; it should work exactly as before. In this case, no visible changes to the app are a good thing; the user can still tap on a row to view and edit a surf session journal entry.

By using a child context as a container for the journal edits, you’ve reduced the complexity of your app’s architecture. With the edits on a separate context, canceling or saving managed object changes is trivial.

Nice work, dude! You’re no longer a kook when it comes to multiple managed object contexts. Bodacious!

Advertisements
Multiple Managed Object Contexts with Core Data Tutorial

Lightweight Migrations in Core Data Tutorial

When you create a Core Data app, you design an initial data model for your app. However, after you ship your app inevitably you’ll want to make changes to your data model. What do you do then? You don’t want to break the app for existing users!

You can’t predict the future, but with Core Data, you can migrate toward the future with every new release of your app. The migration process will update data created with a previous version of the data model to match the current data model.

This Core Data migrations tutorial discusses the many aspects of Core Data migrations by walking you through the evolution of a note-taking app’s data model. You’ll start with a simple app with only a single entity in its data model.

Let the great migration begin!

Note: This tutorial assumes some basic knowledge of Core Data and Swift.

When to Migrate

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

However, there are some cases in which you can avoid a migration. 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.

That said, any time it’s impossible to implement a design change or feature request without changing the data model, you’ll need to create a new version of the data model and provide a migration path.

The Migration Process

When you initialize a Core Data stack, one of the steps involved is adding a store to the persistent store coordinator. When you encounter this step, Core Data does a few things prior to adding the store to the coordinator.

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.

Migrations happen in three steps:

  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.

You might ask, “If something goes wrong, what happens to the original source data store?” With nearly all types of Core Data migrations, nothing happens to the original store unless the migration completes without error. Only when a migration is successful, will Core Data remove the original data store.

Types of Migrations

In my own experience, I’ve found there are a few more migration variants than the simple distinction between lightweight and heavyweight. Below, I’ve provided the more subtle variants of migration names, but these names are not official categories by any means. You’ll start with the least complex form of migration and end with the most complex form.

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.

Manual Migrations

Manual migrations involve a little more work on your part. You’ll need to specify how to map the old set of data onto the new set, but you get the benefit of a more explicit mapping model file to configure. Setting up a mapping model in Xcode is much like setting up a data model, with similar GUI tools and some automation.

Custom Manual Migrations

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.

Fully Manual Migrations

Fully manual migrations are for those times when even specifying custom transformation logic isn’t enough to fully migrate data from one model version to another. Custom version detection logic and custom handling of the migration process are necessary.

Getting Started

Download the starter project for this tutorial here.

Build and run the app UnCloudNotes in the iPhone simulator. You’ll see an empty list of notes:

Tap the plus (+) button in the top-right corner to add a new note. Add a title (there is default text in the note body to make the process faster) and tap Create to save the new note to the data store. Repeat this a few times so you have some sample data to migrate.

Back in Xcode, open the UnCloudNotesDatamodel.xcdatamodeld file to show the entity modeling tool in Xcode. The data model is simple — just one entity, a Note, with a few attributes.

You’re going to add a new feature to the app; the ability to attach a photo to a note. The data model doesn’t have any place to persist this kind of information, so you’ll need to add a place in the data model to hold onto the photo. But you already added a few test notes in the app. How can you change the model without breaking the existing notes?

It’s time for your first migration!

Note: The Xcode 8 console logs contain far more information than in previous releases. The workaround to this issue is to add OS_ACTIVITY_MODE=disable to the current scheme environment variables.

A Lightweight Migration

In Xcode, select the UnCloudNotes data model file if you haven’t already selected it. This will show you the Entity Modeler in the main work area. Next, open the Editor menu and select Add Model Version…. Name the new version UnCloudNotesDataModel v2 and ensure UnCloudNotesDataModel is selected in the Based on model field. Xcode will now create a copy of the data model.

Note: You can give this file any name you want. The sequential v2, v3, v4, et cetera naming helps you easily tell the versions apart.

This step will create a second version of the data model, but you still need to tell Xcode to use the new version as the current model. If you forget this step, selecting the top level UnCloudNotesDataModel.xcdatamodeld file will perform any changes you make to the original model file. You can override this behavior by selecting an individual model version, but it’s still a good idea to make sure you don’t accidentally modify the original file.

In order to perform any migration, you want to keep the original model file as it is, and make changes to an entirely new model file.

In the File Inspector pane on the right, there is a selection menu toward the bottom called Model Version. Change that selection to match the name of the new data model, UnCloudNotesDataModel v2:

Once you’ve made that change, notice in the project navigator the little green check mark icon has moved from the previous data model to the v2 data model:

Core Data will try to first connect the persistent store with the ticked model version when setting up the stack. If a store file was found, and it isn’t compatible with this model file, a migration will be triggered. The older version is there to support migration. The current model is the one Core Data will ensure is loaded prior to attaching the rest of the stack for your use.

Make sure you have the v2 data model selected and add an image attribute to the Note entity. Set the attribute’s name to image and the attribute’s type to Transformable.

Since this attribute is going to contain the actual binary bits of the image, you’ll use a custom NSValueTransformer to convert from binary bits to a UIImage and back again. Just such a transformer has been provided for you in ImageTransformer. In the Value Transformer Name field in the Data Model Inspector on the right of the screen, enter ImageTransformer and enter UnCloudNotes in the Module field.

Note: When referencing code from your model files, just like in Xib and Storyboard files, you’ll need to specify a module (UnCloudNotes in this case) to allow the class loader to find the exact code you want to attach.

The new model is now ready for some code! Open Note.swift and add the following property below displayIndex:

@NSManaged var image: UIImage?

Build and run the app. You’ll see your notes are still magically displayed! It turns out 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. What a time saver!

Inferred Mapping Models

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)
Note: Check out Apple’s documentation for more information on how Core Data infers a lightweight migration mapping:https://developer.apple.com/library/Mac/DOCUMENTATION/Cocoa/Conceptual/CoreDataVersioning/Articles/vmLightweightMigration.html

As you see from this list, Core Data can detect, and more importantly, automatically react to, a wide variety of common changes between data models. As a rule of thumb, all migrations, if necessary, should start as lightweight migrations and only move to more complex mappings when the need arises.

As for the migration from UnCloudNotes to UnCloudNotes v2, the image property has a default value of nil since it’s an optional property. This means Core Data can easily migrate the old data store to a new one, since this change follows item 3 in the list of lightweight migration patterns.

Image Attachments

Now the data is migrated, you need to update the UI to allow image attachments to new notes. Luckily, most of this work has been done for you. :]

Open Main.storyboard and find the Create Note scene. Underneath, you’ll see the Create Note With Images scene that includes the interface to attach an image.

The Create Note scene is attached to a navigation controller with a root view controller relationship. Control-drag from the navigation controller to the Create Note With Images scene and select the root view controller relationship segue.

This will disconnect the old Create Note scene and connect the new, image-powered one instead:

Next, open AttachPhotoViewController.swift and add the following method to the UIImagePickerControllerDelegate extension:

func imagePickerController(_ picker: UIImagePickerController,
  didFinishPickingMediaWithInfo info: [String: Any]) {

  guard let note = note else { return }

  note.image =
    info[UIImagePickerControllerOriginalImage] as? UIImage

  _ = navigationController?.popViewController(animated: true)
}

This will populate the new image property of the note once the user selects an image from the standard image picker.

Next, open CreateNoteViewController.swift and replace viewDidAppear with the following:

override func viewDidAppear(_ animated: Bool) {
  super.viewDidAppear(animated)

  guard let image = note?.image else {
    titleField.becomeFirstResponder()
    return
  }

  attachedPhoto.image = image
  view.endEditing(true)
}

This will display the new image if the user has added one to the note.

Next, open NotesListViewController.swift and update tableView(_:cellForRowAt) with the following:

override func tableView(_ tableView: UITableView,
                        cellForRowAt indexPath: IndexPath)
                        -> UITableViewCell {

  let note = notes.object(at: indexPath)
  let cell: NoteTableViewCell
  if note.image == nil {
    cell = tableView.dequeueReusableCell(
      withIdentifier: "NoteCell",
      for: indexPath) as! NoteTableViewCell
  } else {
    cell = tableView.dequeueReusableCell(
      withIdentifier: "NoteCellWithImage",
      for: indexPath) as! NoteImageTableViewCell
  }

  cell.note = note
  return cell
}

This will dequeue the correct UITableViewCell subclass based on the note having an image present or not. Finally, open NoteImageTableViewCell.swift and add the following to updateNoteInfo(note:):

noteImage.image = note.image

This will update the UIImageView inside the NoteImageTableViewCell with the image from the note.

Build and run, and choose to add a new note:

Tap the Attach Image button to add an image to the note. Choose an image from your simulated photo library and you’ll see it in your new note:

The app uses the standard UIImagePickerController to add photos as attachments to notes.

Note: To add your own images to the Simulator’s photo album, drag an image file onto the open Simulator window. Thankfully, the iOS 10 Simulator comes with a library of photos ready for your use. :]

If you’re using a device, open AttachPhotoViewController.swift and set the sourceType attribute on the image picker controller to .camera to take photos with the device camera. The existing code uses the photo album, since there is no camera in the Simulator.

Lightweight Migrations in Core Data Tutorial

NSFetchedResultsController Core Data Swift 3

Why Use NSFetchedResultsController?

So far, we’re at exactly the same point we were using the SQLite3 method. However, we didn’t have to write nearly as much code (notice the absence of a FailedBankDatabase class constructing raw SQL statements), and adding other functionality such as insert/delete operations would be much simpler.

However, there’s one notable thing that we could add pretty easily with Core Data that could give us huge benefits to performance: use NSFetchedResultsController.

Right now we’re loading all of the FailedBankInfo objects from the database into memory at once. That might be fine for this app, but the more data we have the slower this will be, and could have a detrimental impact to the user.

Ideally we’d like to load only a subset of the rows, based on what the user is currently looking at in the table view. Luckily, Apple has made this easy for us by providing a great utility class called NSFetchedResultsController.

So, start by opening up file and adding a new NSFetchedResultsController instead, Ok, now onto the fun part – creating our fetched results controller!

 fileprivate lazy var fetchedResultsController : NSFetchedResultsController<Messages> = {
 // Initialize Fetch Request
 let fetchRequest: NSFetchRequest<Messages> = Messages.fetchRequest()
 
 fetchRequest.predicate = NSPredicate(format: "topic.userId == %d && topic.status == %d && topic.type == %d", argumentArray: [self.ForUser, 0, 1])
 
 // Add Sort Descriptors
 let sortDescriptor = NSSortDescriptor(key: "time", ascending: true)
 fetchRequest.sortDescriptors = [sortDescriptor]
 
 // Initialize Fetched Results Controller
 let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: self.context, sectionNameKeyPath: nil, cacheName: nil)
 fetchedResultsController.delegate = self
 
 return fetchedResultsController
 }()

This should look pretty familiar to the code that we used to have in viewDidLoad, to create a fetch request to pull out the FailedBankInfo objects. However, there’s a lot of new stuff here so let’s discuss…

First, any time we use an NSFetchedResultsController, we need to set a sort descriptor on the fetch request. A sort descriptor is just a fancy term for an object we set up to tell Core Data how we want our results sorted.

The cool thing about sort descriptors is they are very powerful. Not only can you sort on any property of the object you are returning, but you can sort on properties of related objects – just like we see here! We want to sort the objects based on the close date in the FailedBankDetails, but still only receive the data in FailedBankInfo – and Core Data can do this!

A very important part is the next statement – to set the batch size on the fetch request to some small size. In fact, this is the very reason we want to use the fetched results controller in this case. This way, the fetched results controller will only retrieve a subset of objects at a time from the underlying database, and automatically fetch mroe as we scroll.

So once we finish tweaking the fetch request with the sort descriptor and batch size, we just create a NSFetchedRequestController and pass in the fetch request. Note it takes a few other parameters too:

  • For the managed object context, we just pass in our context.
  • The section name key path lets us sort the data into sections in our table view. We could sort the banks by State if we wanted to, for example, here.
  • The cacheName the name of the file the fetched results controller should use to cache any repeat work such as setting up sections and ordering contents.

So now that we have a method to return a fetched results controller, let’s modify our class to use it rather than our old array method. First, update viewDidLoad as follows:

//Activate fetchedResultsController
 do {
 try fetchedResultsController.performFetch()
 } catch {
 let fetchError = error as NSError
 print("Unable to Fetch Messages")
 print("\(fetchError), \(fetchError.localizedDescription)")
 }

All we do here is get a handle to our fetchedResultsController (which implicitly creates it as well) and call performFetch to retrieve the first batch of data.

Then, update numberOfRowsInSection:

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
 return fetchedResultsController.sections![section].numberOfObjects
 }

And update cellForRowAtIndexPath like the following:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath)
//fetch message
 let message = fetchedResultsController.object(at: indexPath)

cell.LblMessage.text = message.message

return cell
}

Ok one more thing – we need to implement the delegate methods for the NSFetchedResultsController.

func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
 //Begin table update on will change content
 tableView.beginUpdates()
 }
 
 func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
 
 //Reload table indexpaths accorgind to the change in database
 switch type {
 
 case .insert:
 tableView.insertRows(at: [newIndexPath!], with: .fade)
 
 case .update:
 tableView.reloadRows(at: [indexPath!], with: .fade)
 
 default:
 tableView.reloadData()
 }
 
 }
 
 func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
 //End table update on did change content
 tableView.endUpdates()
 }

Compile and run your project, and it should look the same. However, if you examine the debug output you will see something very amazing…

SELECT 0, t0.Z_PK FROM ZFAILEDBANKINFO t0 LEFT OUTER JOIN
    ZFAILEDBANKDETAILS t1 ON t0.ZDETAILS = t1.Z_PK
    ORDER BY t1.ZCLOSEDATE DESC
total fetch execution time: 0.0033s for 234 rows.

SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZNAME, t0.ZSTATE, t0.ZCITY,
    t0.ZDETAILS FROM ZFAILEDBANKINFO t0 LEFT OUTER JOIN
    ZFAILEDBANKDETAILS t1 ON t0.ZDETAILS = t1.Z_PK WHERE
    t0.Z_PK IN  (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
    ORDER BY t1.ZCLOSEDATE DESC LIMIT 20
total fetch execution time: 0.0022s for 20 rows.

SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZNAME, t0.ZSTATE, t0.ZCITY,
    t0.ZDETAILS FROM ZFAILEDBANKINFO t0 LEFT OUTER JOIN
    ZFAILEDBANKDETAILS t1 ON t0.ZDETAILS = t1.Z_PK WHERE
    t0.Z_PK IN  (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
    ORDER BY t1.ZCLOSEDATE DESC LIMIT 20
total fetch execution time: 0.0017s for 20 rows.

You can see here that behind the scenes, the NSFetchedResultsController is getting a list of IDs from FailedBankInfo, in the proper sort order. Then, as the user pages through the table, it loads one batch at a time – rather than loading all of the objects into memory at once!

This would have been a lot more code to do with raw SQLite – and is just one of the many reasons why using Core Data can save time and increase performance.

NSFetchedResultsController Core Data Swift 3

Core Data Delete Rules Swift 3

Delete Rules

What happens if a note is deleted? Should the category the note belongs to also be deleted? No. But what happens if a category is deleted? Should it be possible to have notes without a category?

This brings us to delete rules. Every relationship has a delete rule. A delete rule defines what happens when the record that owns the relationship is deleted.

A delete rule defines what happens when the record that owns the relationship is deleted.

Select the notes relationship of the Category entity and open the Data Model Inspector on the right. By default, the delete rule of a relationship is set to nullify. Core Data supports four delete rules:

  • No Action
  • Nullify
  • Cascade
  • Deny

No Action Delete Rule

If the delete rule of a relationship is set to No Action, nothing happens. Let me illustrate this with an example. We have a category that contains several notes. If the category is deleted, the notes are not notified of this event. The notes on the other end of the relationship believe that they are still associated with the deleted category.

I have never had a need to use this delete rule in a project. In most situations, you want to take some action when a record is deleted. And that is where the other delete rules come into play.

Nullify Delete Rule

If the delete rule of a relationship is set to Nullify, the destination of the relationship is nullified when the record is deleted.

For example, if a category has several notes and the category is deleted, the relationships pointing from the notes to the category are nullified. This is the default delete rule and the delete rule you will find yourself using most often.

Cascade Delete Rule

The Cascade delete rule is useful if the data model includes one or more dependencies. Let me give you an example. If a note should always have a category, the deletion of a category should automatically delete the notes associated with that category. In other words, the deletion of the category cascades or trickles down to the notes linked to the category. Even though this may make sense on paper, the user probably won’t like it when you automatically delete its notes. The Deny delete rule is a better option in this scenario (see below).

If you are dealing with a Many-To-Many relationship, this is often not what you want. If a note can have several tags and a tag can be linked to several notes, deleting a tag should not result in the deletion of every note with that tag. The notes could be associated with other tags, for example.

Deny Delete Rule

Remember the previous example in which the deletion of a category resulted in the deletion of every note that belonged to that category. It may be better to apply the Deny delete rule.

Deny is another powerful and useful pattern. It is the opposite of the Cascade delete rule. Instead of cascading the deletion of a record, it prevents the deletion of the record.

For example, if a category is associated with several notes, the category can only be deleted if it is no longer tied to any notes. This configuration prevents the scenario in which notes are no longer associated with a category.

Core Data Delete Rules Swift 3

Write Mockup Classes For Unit Testing Of UserDefaults, Core Data & UrlSessions In Swift 3 & XCode 8

URLSession

Source Code

public protocol CLSession {
func dataTask(with url: URL, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask
func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask
}

extension URLSession: CLSession { }

public final class URLSessionMock: CLSession {

var url: URL?
var request: URLRequest?
private let dataTaskMock: URLSessionDataTaskMock

public convenience init?(jsonDict: [String: Any], response: URLResponse? = nil, error: Error? = nil) {
guard let data = try? JSONSerialization.data(withJSONObject: jsonDict, options: []) else { return nil }
self.init(data: data, response: response, error: error)
}

public init(data: Data? = nil, response: URLResponse? = nil, error: Error? = nil) {
dataTaskMock = URLSessionDataTaskMock()
dataTaskMock.taskResponse = (data, response, error)
}

public func dataTask(with url: URL, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
self.url = url
self.dataTaskMock.completionHandler = completionHandler
return self.dataTaskMock
}

public func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
self.request = request
self.dataTaskMock.completionHandler = completionHandler
return self.dataTaskMock
}

final private class URLSessionDataTaskMock : URLSessionDataTask {

typealias CompletionHandler = (Data?, URLResponse?, Error?) -> Void
var completionHandler: CompletionHandler?
var taskResponse: (Data?, URLResponse?, Error?)?

override func resume() {
DispatchQueue.main.async {
self.completionHandler?(self.taskResponse?.0, self.taskResponse?.1, self.taskResponse?.2)
}
}
}

}

Images
Use

UserDefaults

Source Code

class MockUserDefaults: UserDefaults {
var loggedInUser = 0
override func set(_ value: Any?, forKey defaultName: String) {
if defaultName == “loggedInUser” {
loggedInUser += 1
}
}
}

Images

Use

CoreData

Source Code

// MARK: – Variables
//Core Data variables
var storeCoordinator: NSPersistentStoreCoordinator!
var managedObjectContext: NSManagedObjectContext!
var managedObjectModel: NSManagedObjectModel!
var store: NSPersistentStore!

//Managers
var apiManager: APIManager!
var dataManager: DataManager!

// MARK: – XCTest Methods

override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
//Set Managers
apiManager = APIManager.shared
dataManager = DataManager.shared

/* Core Data Mock Object Configuration */
// ——– Start ——-
managedObjectModel = NSManagedObjectModel.mergedModel(from: nil)
storeCoordinator = NSPersistentStoreCoordinator(managedObjectModel: managedObjectModel)

do {
store = try storeCoordinator.addPersistentStore(ofType: NSInMemoryStoreType, configurationName: nil, at: nil, options: nil)
} catch {
print(error.localizedDescription)
}

managedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
managedObjectContext.persistentStoreCoordinator = storeCoordinator
apiManager.context = managedObjectContext
dataManager.context = managedObjectContext
(UIApplication.shared.delegate as! AppDelegate).mockContext = managedObjectContext
// ——– End ——-
}

override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
managedObjectContext = nil
apiManager = nil
dataManager = nil
(UIApplication.shared.delegate as! AppDelegate).mockContext = nil

//Check Store Removal
do {
try storeCoordinator.remove(store)
} catch {
XCTFail(“couldn’t remove persistant store: \(error)”)
}

super.tearDown()
}

func testThatStoreIsSetUp() {
XCTAssertNotNil(store, “no persitant store”)
}

Images

Use

Write Mockup Classes For Unit Testing Of UserDefaults, Core Data & UrlSessions In Swift 3 & XCode 8

Core Data Using Swift 3

User Data Model

user_data_model

Product Data Model

product_data_model

Data Model Structure

model_structure

Core Data Stack

core_data_stack

Core Data Saving Support

core_data_saving_support

Get Context To Work On Core Data Stack

persistance_store_context

Save Single Record

save_single_record

 Save Context To Update Database

save_context_for_update_database

Fetch Single Record

fetch_single_record

Add/Update Multiple Record (One-To-Many Relation)

save_update_multiple_records

Fetch Multiple Records (One To Many Relation)

fetch_multiple_record_one_to_many

Relations

Library Data Model

The Library entity has two attributes, name and location, both of type String. The Author entity also has two attributes, firstName and lastName, both of type String. The Book entity has three attributes, title of type String, publicationDate of type Date, and pages of type Integer 16. This is what the data model should look like in the data model graph.

figure-data-model-1

Planning Ahead

A library contains books and books are written by authors. That is the idea underlying the data model we are creating. This implies that the Author entity is tied to the Book entity. The Library and Book entities are also linked.

But this is one possible solution. We can also say that a library contains the books of authors and that authors have written books. Do you see the difference? In that scenario, the Library entity has a relationship with the Author entity, not the Bookentity.

When working with data models, it is critical to spend some time planning and thinking what the data model should look like. Once you have decided on the data model, it is often expensive to make drastic changes to it. We learn more about what that means for Core Data when we discuss migrations.

When working with data models, it is critical to spend some time planning and thinking what the data model should look like.

Relationships

To-One Relationships

A book belongs to a library. This means that we need to create a relationship that links a book to a library. Open Library.xcdatamodeld and switch to the editor’s tablestyle. Select the Book entity and click the plus button at the bottom of the Relationships table to add a relationship to the entity. Set Relationship to library and Destination to the Library entity. Leave Inverse empty for now.

figure-add-to-one-relationship-1

Switch to the editor’s graph style to see the visual representation of the relationship we defined.

figure-add-to-one-relationship-2

Before we move on, there are a number of details we need to discuss. The relationship we added is a to-one relationship, a book can belong to only one library. This is visualized in the data model graph by the single arrow pointing from the Bookentity to the Library entity.

Note that the arrow points from Book to Library. There is no arrow pointing back from Library to Book. There is no inverse relationship because we haven’t defined one yet. This implies that the library the book belongs to does not know that the book belongs to the library. That is not what we want, though. A library should know what books it contains. Right?

Inverse Relationships

Switch to the editor’s table style and select the Library entity. Add a relationship and name it books, plural. Set Destination to Book and set Inverse to library. By setting the inverse relationship to library, the inverse relationship of the books relationship is automatically set to the library relationship of the Book entity.

figure-add-inverse-relationship-1

Switch to the data model graph to see what that looks like. The connection between Library and Book contains arrows pointing to and from each entity.

figure-add-inverse-relationship-2

Switch back to the editor’s table style and select the Book entity. Note that the inverse relationship of the library relationship is automatically set to books because we set the inverse relationship of the books relationship to library. Core Data is clever enough to take care of that for us.

To-Many Relationships

Both library and books are to-one relationships. This severely limits the data model. A library with only one book isn’t much of a library. It is time to fix that. Select the books relationship of the Library entity in the editor’s table style. Open the Data Model Inspector in the Utilities pane on the right to see the details of the relationship.

In the Data Model Inspector, you can modify the relationship’s destination and inverse relationship. You can also modify the relationship’s type or cardinality. Core Data supports To-One and To-Many relationships. Set Type to To-Many.

figure-add-to-many-relationship-1

Switch back to the data model graph to see what has changed. The relationship from Library to Book now has two arrows, indicating that books is a to-many relationship.

figure-add-to-many-relationship-2

Many-To-Many Relationships

Wait a second. Can a book only be in one library? That may be true for an exceptionally rare book. We need to turn the library relationship into a to-manyrelationship. Select the library relationship of the Book entity in the editor’s table style and set Type to To-Many in the Data Model Inspector.

Naming conventions are very important, especially if you are working in a team. Even though the library relationship is now a to-many relationship, the name of the relationship doesn’t reflect this change. Update the name of the relationship to libraries. That makes more sense. Developers new to the project will immediately understand that a book can belong to multiple libraries.

This is what the data model graph looks like after the changes. The relationship that links the Library and Book entities is now a many-to-many relationship.

figure-add-many-to-many-relationship-1

Delete Rules

Core Data is much more than a database and this becomes clear when working with relationships. Relationships in Core Data are powerful because the framework does a lot of the heavy lifting for you.

Delete rules are one of the conveniences that make working with Core Data great. Every relationship has a delete rule. It defines what happens when the record that owns the relationship is deleted. Let me explain what that means.

Select the books relationship of the Library entity and open the Data Model Inspector on the right. By default, the delete rule of a relationship is set to nullify. Core Data supports four delete rules:

  • No Action
  • Nullify
  • Cascade
  • Deny

No Action

If the delete rule of a relationship is set to No Action, nothing happens. Let me illustrate this with an example. We have a library record that contains several book records. If the library record is deleted, the book records are not notified of this event. Every book record thinks it is still associated with the deleted library.

I have never had a need to use this delete rule. In most situations, you want to take some action when a record is deleted. That is where the other delete rules come into play.

Nullify

If the delete rule of a relationship is set to Nullify, the destination of the relationship is nullified when the record is deleted. If a library has several books and the library is deleted, the relationship pointing from the books to the library is nullified. This is the default delete rule and the delete rule you will find yourself using most often.

Cascade

Cascade is useful if the data model includes one or more dependencies. For example, if an account record has a relationship to one or more user records, it may be desirable to delete every user if the account the user belongs to is deleted. In other words, the deletion of the account record cascades to the user records it is linked to.

If you are dealing with a many-to-many relationship, though, this is often not what you want. If a library with several books is deleted, you don’t want to delete every book in the library. The books could be associated with other libraries, for example.

Deny

Deny is another powerful and useful pattern. It is the opposite of the Cascade delete rule. Instead of cascading the deletion of a record, it prevents the deletion of a record. For example, if an account is associated with several users, the account can only be deleted if it is no longer tied to any users. This configuration prevents the scenario in which users are no longer associated with an account.

Core Data Using Swift 3