Chapters

Hide chapters

Core Data by Tutorials

Eighth Edition · iOS 14 · Swift 5.3 · Xcode 12

Core Data by Tutorials

Section 1: 11 chapters
Show chapters Hide chapters

10. NSPersistentCloudKit Container
Written by Aaron Douglas

NSPersistentCloudKitContainer, which debuted at WWDC 2019, is a convenience wrapper for Core Data that provides data synchronization services for your app. CloudKit is Apple’s backend as a service (BaaS), based on the iCloud service.

Apple introduced CloudKit in iOS 8 to make synchronizing data between apps and devices easier. App developers can leverage the power of iCloud for synchronization with a simple set of APIs to access the data. CloudKit has APIs for iOS, macOS and JavaScript for the web.

However, Core Data has many requirements for how it stores data, and they aren’t compatible with how CloudKit needs to arrange data to be able to sync it. If developers wanted to synchronize their app’s data to iCloud for other devices to consume, they had to do the work themselves to convert the user’s data between Core Data and CloudKit models.

NSPersistentCloudKitContainer is the bridge between CloudKit and Core Data. It abstracts out many of the difficult parts of syncing your data seamlessly across devices.

Understanding CloudKit’s strengths

Core Data, as you’ve seen, is a powerful data persistence framework. It’s easy to start using it and to integrate it with your app.

Core Data isn’t just useful for the data that your users type into apps. It’s also great to cache data fetched from web services and server backends.

However, not every app has a complex backend powering it. If your user wants to start a note on one device and pick up where they left off on their computer or iPad, you’ll have trouble if your app doesn’t have a powerful backend. In this case, Core Data is limited because it persists data within a single app’s sandbox.

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.

Preparing to use NSPersistentCloudKitContainer

CloudKit requires a paid Apple Developer account to function. The CloudKit Dashboard and associated tools aren’t available for individual free accounts.

You also need to sign in to iCloud on your simulator to test devices with the Apple ID you used to sign in to your Apple Developer account.

If your test device is your primary device and you aren’t signed into that Apple ID for iCloud, add your regular Apple ID to your Apple Developer account as an additional developer.

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.

You’re familiar with data models living within your app and you know how changes in your app affect migrations between model versions. Your data model also lives within CloudKit in iCloud, not just in the app.

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.

Apple covers suggested approaches to handle schema change management in the CloudKit documentation (https://developer.apple.com/documentation/coredata/mirroring_a_core_data_store_with_cloudkit/creating_a_core_data_model_for_cloudkit).

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.

Getting started with Core Data and CloudKit

To see how easy it is to use CloudKit with Core Data, you’ll create a quick demo app. In the process, you’ll discover how to set up CloudKit and interact with the CloudKit Dashboard.

Setting up your project

Start by creating a new project in Xcode. Select the iOS platform and App in the template chooser.

Enter a Product Name then choose a Team and an Organization Identifier. Select SwiftUI for the interface, and SwiftUI App for the lifecycle. Make sure to check Use Core Data and Host in CloudKit. Click Next. Choose a destination for the project and click Create.

The skeleton SwiftUI app, as of Xcode 12.0.1, does not work as expected. You have to make a few changes to the main view in order for the list and add new row button to render. Open ContentView.swift. Replace the body property with the following:

var body: some View {
  NavigationView {
    List {
      ForEach(items) { item in
        Text(
          "Item at \(item.timestamp!, formatter: itemFormatter)"
        )
      }
      .onDelete(perform: deleteItems)
    }
    .toolbar {
      Button(action: addItem) {
        Label("Add Item", systemImage: "plus")
      }
    }
    .navigationTitle("Items")
  }
}

Selecting the Use CloudKit checkbox when you created the project sets up the app delegate to use NSPersistentCloudKitContainer, but it doesn’t configure any entitlements. You’ll need to do this yourself.

In the Project navigator, click the root project node then click the main Target for the app. Next, select the Signing & Capabilities tab and click the + Capability button in the header. A search box will appear. Search for iCloud and double-click it to add it to the project.

In the newly-added iCloud section, check the CloudKit box under Services then click the + sign under Containers. When prompted, enter the full bundle ID for your app. In this case, that value is com.raywenderlich.CloudKitExample.

The container name will be red when you first add it, but don’t worry. Wait a few moments and click the refresh button — the text will turn black.

Next, click the + Capability button in the header. Search for Background Modes and double-click it to add it to the project.

In the newly-added Background Modes section, check the Remote notifications box.

Now, open Persistence.swift and add the following line at the end of the init(inMemory:) method, right after the loadPersistentStores method is called:

container.viewContext.automaticallyMergesChangesFromParent
  = true

With this change, your UI will update immediately when changes come in over the internet from CloudKit.

To ensure there are no build errors and that the app launches, build and run in a simulator or a device, then once the app has started up, stop it again from Xcode.

CloudKit is aware of your app because you created a container in a previous step. To access it, click the CloudKit Dashboard button under iCloud in Signing & Capabilities. After you sign in with your Apple Developer Apple ID, you’ll see a list of containers. Click on the container name you created earlier.

Click on Schema and you’ll notice there are no Custom Types listed. Custom Types are what CloudKit uses to represent your data model, so they’re pretty important.

Adding custom types

Next, you’ll tell CloudKit about your data model.

There’s no way to upload your Core Data model file directly into the CloudKit Dashboard. Instead, you inform CloudKit of your current schema by launching your app into a simulator or device signed into the iCloud account associated with your Apple Developer ID. When the app launches, it will connect to CloudKit and upload the schema.

If you’re using the simulator, go to System Preferences, tap on Sign into your iPhone and enter your credentials for your Apple Developer Apple ID.

Now you’ve signed into iCloud, go back into Xcode. Build and run the app. When it loads, tap the + button to add a new entry to the table. You should see a line entry similar to the following:

While your app is running, you’re likely to see a slew of debug messages. Most of them aren’t useful to you yet. However, if you scroll back carefully, you should be able to pick out the CloudKit message when the new entry you just created is sent upstream. This means it’s working!

Notice that the Core Data entity named Event becomes CD_Event when it goes into CloudKit. This happens because NSPersistentCloudKitContainer automatically prefixes entity names and field names with CD_.

Return to the CloudKit Dashboard and reload the page. Click on your app’s container, then click on Schema. You’ll see CD_Event listed as a Custom Type. Woohoo!

Viewing your data

The Dashboard also gives you the ability to see the data, not just the schema. Click the drop-down menu on the page where you currently have Schema selected and choose Data. You’ll see the query tool that lets you find data in CloudKit.

In the Type drop-down, find and select CD_Event if it isn’t already selected. Make sure you’ve selected com.apple.coredata.cloudkit.zone above. You want to see all the data, not any particular record, so go ahead and click Query Records.

Uh oh! An error? Don’t worry — you haven’t done anything wrong.

Fixing the error

You get this error because when you first upload your schema, none of the Core Data fields are queryable, nor is the CloudKit field recordName. You’ll change that next.

Head back to the Schema editor. Click on the CD_Event Custom Type and then click Edit Indexes to the right, underneath the Custom Fields section.

Next, click Add Index.

By default, you’ll see the field recordName in the drop-down along with QUERYABLE under Index Type. This is exactly the index and type you need to add, so click Save Changes.

Head back to the Data screen and try clicking Query Records again with the CD_Event type selected. You’ll now see the one record you added in your simulator.

Click the blue text under Name and you’ll see a panel pop up with the entire CloudKit record. Notice how each of the Core Data model fields is listed as a custom field.

Editing and syncing your records

Now, try editing the record through the CloudKit Dashboard — you’re going to test just how magical CloudKit is.

The only visible field in the app on the record is the CD_timestamp field. Click the field and change the date to something different that you’ll remember, then click Save.

Note: The CloudKit data editor is fairly simplistic — it won’t adhere to any business logic or formatting rules from your app’s Core Data model or underlying code. Be very careful when editing the raw data. If you introduce a badly-formatted value, it may crash your app or cause problems for your users.

Switch back to your simulator. Note that, although it’s still running, the date and time haven’t updated to your changed value.

This is because, as mentioned earlier, push and background notifications don’t work in the simulator. On a device, CloudKit sends silent notifications to an app whenever there are new records or edits, but the simulator doesn’t have this feature.

You can force the simulator to check in with CloudKit by temporarily backgrounding the app. Put the app in the background by hitting Command+Shift+H, then bring it back to the foreground by tapping the app’s icon. The date will change after it syncs with iCloud.

Magic, right?

In this section, you created a small test app that demonstrated what it takes to get CloudKit working with Core Data. You got to play around with the CloudKit Dashboard and to see what your Core Data model looks like as a CloudKit schema. You also edited a record to prove sync works.

This is great for a brand-new app — but what about converting an existing app to use CloudKit?

In the next section, you’ll get to do just that, with the soon-to-be-released RayWenderlich.com app, Dog Doodies.

Introducing the Dog Doodies app

When someone gets a new dog, they want to make sure they understand their pet’s needs. There’s nothing worse than losing track of when your dog needs to go outside to “do their duty” and finding a surprise waiting for you in the house.

That’s where Dog Doodies comes in!

You can track each of your dog’s important duties, see a history of them, and see a timer telling you how long since the last one completed.

During our initial user testing, we discovered pretty quickly that having the history on a single device wasn’t enough. People expected the data to be on their watches, their iPad and even their computers. This is a great example of where CloudKit can help.

Find and open the starter project for this section under projects\starter\DogDoodies. Open the Core Data model at Model/DogDoodies.xcdatamodeld.

The Core Data model has two entities: Pet and Activity. Originally, the designers intended the app to work for any pet in your house. User research concluded cats are smarter and don’t need (or want) to be tracked.

A pet has several properties, the most important being its name.

Each pet can have from zero to many Activity objects. An Activity has a type, like pee, poop or walk. It has a field for when the activity took place and a future field for any special notes.

Activity also has a link back to the Pet object, which is great because NSPersistentCloudKitContainer requires inverse relationships. Thankfully, everything in the data model is fully compatible with CloudKit, so you don’t have to make many changes to integrate it into the app.

Now, it’s time to add CloudKit to the app.

Configuring CloudKit

Configuring CloudKit for this project is similar to the configuration you did for the simple demo app.

In Xcode, go to the Signing & Capabilities tab in the project configuration for the DogDoodies main target. Change the bundle identifier to something unique for you, instead of it beginning with com.raywenderlich. Then, after making sure you’ve selected Automatically manage signing, select your Team in the drop-down.

Xcode should think a little bit, then set up Xcode to properly code sign the project.

Next, click the + Capability button in the header. Search for iCloud and add it to the project.

In the newly-added iCloud section, check the CloudKit box under Services, then click the + sign under Containers.

When prompted, enter your app’s full bundle ID, which you modified in the last step.

As before, the container name will be red when you first add it. Wait a few moments, then click the refresh button. The text will turn black.

Next, click the + Capability button in the header. Search for Background Modes and add it to the project. In the newly-added Background Modes section, check the Remote notifications box.

Finally, build and run the app in Xcode to make sure there are no configuration issues.

Great! Now you’re ready to convert your app!

Convert the container to CloudKit

The first and only step to convert Dog Doodies over to CloudKit is to replace the persistent container class with NSPersistentCloudKitContainer.

Open CoreDataStack.swift and replace the storeContainer property definition with the following:

private lazy var storeContainer: NSPersistentContainer = {
  // 1
  let container = 
    NSPersistentCloudKitContainer(name: self.modelName)
  container.loadPersistentStores { _, error in
    if let error = error as NSError? {
      print("Unresolved error \(error), \(error.userInfo)")
    }
  }

  // 2
  container.viewContext.automaticallyMergesChangesFromParent
    = true

  // 3
  do {
    try container.viewContext.setQueryGenerationFrom(.current)
  } catch {
    fatalError("###\(#function): Failed to pin viewContext to the current generation:\(error)")
  }

  return container
}()

In the code above, you:

  1. Replaced NSPersistentContainer with NSPersistentCloudKitContainer. You could stop here and the app would function with CloudKit.

  2. Turned on the option to automatically merge changes, as in the previous app. In some scenarios, automatic merging is not optimal for your app — for example, if you have a lot of custom validation logic or if you have tricky merge conflicts to manage with user intervention. For your simple app, this option will work well.

  3. Pinned the managed object context used for all UI work on the main thread to the current generation of data from CloudKit.

Pinning a context prevents the UI from changing while the user is interacting with it. This avoids situations like when a user tries to tap on a row and an incoming change adds another row, causing the wrong record to display.

Any time you save, merge into or reset a context, the pinned generation automatically updates. Since you turned on automatic merging in the previous step, pinning the UI won’t have much visible effect.

If you have a particular screen that should “pause” updates, create a new view context for that UI and pin it, but turn off automatic merging for it.

Log some doodies!

Build and run the app. After it launches, you’ll see a “No Dog Selected” message.

Tap the Select Dog button to bring up the currently-empty list of dogs in the app. Tap the + button and enter Applesauce as the name for your dog. Tap Add and then tap the row in the table to select that dog.

Pretend you just took Applesauce outside to use the bathroom. She drank a lot of coffee that morning, so she only had to pee. Tap the water droplet button. You’ll see the clock start to count up, which tells you how long it’s been since the last time she peed. Tap the History button to see more details about your dog.

For this next step, you’ll run the app on a different simulator or on an actual iOS device. Before launching the app, make sure to log into the same Apple ID/iCloud account on that target. Once you’ve configured it, select that simulator or device in Xcode and build and run the app while keeping the original simulator running.

Notice that when the app launches, the countdown timer is already running. When you tap History, the activity you created is already listed! Amazing, right?

Tap one of the other two buttons to log another activity. In the other simulator, background the app by pressing Shift+Command+H and then bring it back into the foreground. The timer for the newly-created activity should start counting down and closely match the timer in the other device.

You can also delete activities by swiping from right to left on the activity row, then tapping Delete. CloudKit will keep track of deleted records so that all the clients connected to that container can replay the action.

Working with deleted data is particularly tricky, and it has kept many developers up late at night to solve it in their apps. The fact that it works out of the box with Core Data and CloudKit saves you so much effort. Thanks, Apple!

Looking at CloudKit data

What does all this data look like in CloudKit?

Click the CloudKit Dashboard button in the iCloud configuration section in Xcode to return to the dashboard. Find the container you created for the Dog Doodies app and select it. Click Schema and then click Edit Indexes. Add an index on recordName with the QUERYABLE type for both the CD_Pet and CD_Activity custom record types. Finally, click Save Changes before going to the next record type.

Switch to the Data viewer and query for all the Pet records. Click on the details and you’ll see all the fields you expect to see: the pet’s name, type of animal and the entity name. You may also notice a Boolean for visible. This is in the Core Data model, but it isn’t yet used in the app. It’s populated in CloudKit because the attribute is configured to have a default value of true. If the app sells well, eventually you might need a setting for when Applesauce “goes to live on a farm” :(

Now, query for all the Activity records and click on the details for one activity. You’ll see that there’s a reference for CD_Pet with a GUID. That’s the way CloudKit keeps referential integrity in place, by simply saving the unique ID of the related record in it.

There’s one field that you don’t see in Activity, which is the notes field. If your field in Core Data is an optional field and you didn’t provide any data for it, that field may not show up in CloudKit.

Understanding CloudKit’s weaknesses

Like most things in life, if something seems too easy, you probably did it wrong. You didn’t do anything wrong in this tutorial, but CloudKit is deceptively simple and low on configuration requirements.

Entitlements and code signing are tricky, especially if you aren’t letting Xcode manage those things automatically. The complexity lies in the details, and those are easy to overlook initially.

Here are a few issues to keep in mind when you work with CloudKit.

Model versioning

Over time, your data model will change. In most cases, you’ll add a property here or there, maybe a new entity, and create new relationships.

Core Data’s model versioning and migration framework is powerful and it can handle many complex scenarios. One drawback to CloudKit is that it does not directly support Core Data’s built-in model versioning system.

As mentioned earlier in this chapter, CloudKit lets you add fields and entities, but not rename those fields or delete those entities. As your app grows over time, you’ll need to account for this in how your model changes, if you decide to keep a single model in the app.

Remember that your app may be running on a wide range of devices, operating system versions and user scenarios. Don’t assume that users will upgrade their app to the newest version when you release it to the App Store. If your production schema continues to change over time, old versions of the app won’t know about new fields or entities. Your users may also have a mix of app versions running across their devices, and they’ll expect them to “just work” and behave.

Apple offers some solutions, like including a version number in each of your entity’s data models. This lets you perform conditional logic within the app.

If your app is complex, consider having a separate Core Data Configuration or entire model/stack for your CloudKit store. This lets your synced data model change less often, and lets you target specific data that needs to synchronize, rather syncing than the entire store.

There are certainly drawbacks to this scenario as well: You’ll need to transfer data from one stack to another and there’s a chance of merge conflicts that you’ll need to either ignore or have the user deal with. Your entity names also need to be different, unless you put them into different Swift modules.

There’s no clear answer about what’s right for your app — you’ll need to discover what works best for your users.

Automatically merging

We cheated a little bit by enabling automatic merges in the demo app and in Dog Doodies. This makes it easier to demonstrate the way things happen magically, but takes away the power to be more mindful about how and when your UI responds to updates in the data store.

Should your entire app update the UI when a merge happens? What if the data change doesn’t affect anything currently on display?

Automatic merging can affect perceived performance for the user as well. You may need to consider letting the user decide how to fix merge conflicts instead of just overwriting the data.

If you need to be more mindful of handling and merging incoming data, look into Apple’s Persistent History Tracking mechanism in Core Data. By turning on this option, you can inspect the incoming changes in your context to see if they’re relevant for the current view. If they aren’t, simply ignore them.

Persistent History Tracking isn’t a simple feature to start using. There are a lot of edge cases around the mechanism, which can make testing it a bit trickier. It would take a whole new chapter of this book to cover it… send in your requests :]

Debugging CloudKit

Inevitably, something will go wrong with a data sync and you’ll need to dig into the sync mechanism. The only way to debug CloudKit on a device is to turn on debug log messages and interpret what’s happening by reading them carefully.

Turn logging on and define how much logging CloudKit displays by adding the com.apple.CoreData.CloudKitDebug launch argument in your target’s Run scheme configuration.

Adding -com.apple.CoreData.CloudKitDebug 1 as an argument gives you the least amount of debugging information. The number after the argument name indicates the amount of logging, with 1 being the lowest value and 3 or 4 being the highest — Apple isn’t clear on the upper limit.

Testing on iCloud

You’ll need to test your app before releasing it to production, and this isn’t very easy on iCloud, especially if you aren’t the only developer on the project.

iCloud, in general, isn’t set up well to work with test devices and test scenarios. Apple Developer accounts require that you enable two-factor authentication on your Apple ID. Setting up test Apple IDs often triggers fraud locks because of the way developers need to log in and out of accounts on test devices.

Key points

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

Where to go from here?

Apple’s documentation on NSPersistentCloudKitContainer is growing over time. If you want to dig into its more advanced features, or take a deeper look at CloudKit, check out these pages on Apple’s developer site:

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.