Syncing data on iOS devices with CoreData and CloudKit

A tutorial to learn how to store and synchronize data across iOS devices with apps targeting iOS 13+

Janin Culhaoglu
Apple Developer Academy | Federico II
8 min readApr 9, 2020

--

Have you noticed that when you create a Note on your device, the Note appears on your other devices? Syncing data between iOS devices has become a simple process thanks to iCloud.

Many iOS apps use iCloud to store data. This makes their information easier to synchronize across your devices when you install the same app.

You might wonder which of your apps use iCloud syncing. You can easily see the list on your device.

iCloud settings — Apps using iCloud

How to create an app which stores user data to iCloud?

We will explore the framework from Apple: CloudKit. This framework provides interfaces for moving data between your app and your iCloud containers.

To give you an example, we will create a very simple To Do List app where the user can add a new task to the list. Those tasks will be synchronized in the user’s other device thanks to iCloud.

Step by step, we will create this project and save tasks with a Core Data Entity and send it to the cloud with the NSPersistentCloudKitContainer class.

I provide the Xcode project at the end of the article.

To Do List App Screenshots

Requirements

To perform this tutorial, you need a mac computer with Xcode and an iCloud account to use for development.

You need to be targeting iOS 13+ in order tu use NSPersistentCloudKitContainer in your app.

Tutorial to make a ToDoList App

1. Setup the Project

2. Create the interface and connect elements to the ViewController

3. Create a Core Data Entity

4. Refactoring AppDelegate.swift

5. Make the Task extension work

6. Save task in local storage

7. Adding Capabilities

8. Result

1. Setup the Project

The first step is to create a new project based on the Single View App template. Next, name the project, select your team name, and select “Use Core Data” and “Use CloudKit” checkboxes.

Xcode generates some code in the AppDelegate.swift file to initialize the Core Data stack with this new class since iOS 13: NSPersistentCloudKitContainer.

Extract of code generated in AppDelegate.swift

The only difference by selecting CloudKit checkbox at the project creation is that instead of initializing an NSPersistentContainer, Xcode uses the new NSPersistentCloudKitContainer. If you forgot to select this, you can change it manually in the AppDelegate file (NSPersistentContainer to NSPersistentCloudKitContainer).

2. Create the interface and connect elements to the ViewController

In Main.storyboard:

  1. Add elements: Text Field, “Add” Button, Table View
  2. Select the controller, go to Editor/Embed in/Navigation Controller
  3. Change the navigation bar style to “Prefers large titles”
  4. Add a bar button item on it. Change the text to “Reset”
  5. Select the table view and CTRL + drag to add dataSource and delegate
Main.storyboard file

In Viewcontroller.swift:

Connect elements with IBOutlet and IBAction

ViewController.swift file

3. Create a Core Data Entity

Select the file with the extension xcdatamodeld

SaveList.xcdatamodeld

In this file, we will define our objects in the database. How are they called? What are their properties? What are the relationships between them?

The things to know are that we are talking about entities for objects and attributes for their properties.

The NSManagedObjectModel:

In our case, we want the user to add tasks in the ToDoList app. So we need to create the Task object with his taskName property.

Click on the (+) button to add an Entity and name it Task. Add new attribute taskName and set his type to String.

Select Task Entity, and on the right menu, choose “Category/Extension” for Codegen.

Choosing Category/Extension from the Codegen menu, tells Xcode to create a Swift class extension for the entity when you build the project. The point is to automatically update the files when you make changes to the data model.

The next step is to create Swift class extension files for Task entity and add methods there. But before moving on, I would like to simplify some code in AppDelegate.swift file to make it less redundant to use.

4. Refactoring AppDelegate.swift

In fact, every time we want to access our persistentContainer property, we will have to go through:

(UIApplication.shared.delegate as! AppDelegate).persistentContainer

I suggest that we offer a static property to simplify all of this:

static var persistentContainer: NSPersistentContainer {return (UIApplication.shared.delegate as! AppDelegate).persistentContainer}

Now we can access to persistentContainer by just writing:

AppDelegate.PersistentContainer

It is more practical!

The last step is to recover the context, since it is in the context that we are going to manipulate the data.

To do this, we will simply use the viewContext property of type NSManagedObjectContext of the NSPersistentContainer class. This property returns a context to use in the Main Thread, the one in which the views are managed.

For the same practical reasons, I suggest that we create a static property in AppDelegate:

static var viewContext: NSManagedObjectContext {
return persistenceContainer.viewContext
}

Now we can use our context everywhere in our code by simply writing AppDelegate.viewContext.

Your AppDelegate should be similar to this:

5. Make the Task extension work

To make this Task extension work, we need to create our Task class. So I let you add a new swift file and name it Task. You need to write “import CoreData” to be able to use this Framework.

For Task to be an object in the sense of Core Data, it must inherit from the NSManagedObject class:

import Foundation
import CoreData
class Task: NSManagedObject {}

To retrieve data, we will use the NSFetchRequest class. This class is used to create a query.

Add the code below inside your Task class:

static func fetchAll(viewContext: NSManagedObjectContext = AppDelegate.viewContext) -> [Task] {let request : NSFetchRequest<Task> = Task.fetchRequest()request.sortDescriptors = [NSSortDescriptor(key: “taskName”, ascending: true)]guard let tasks = try? AppDelegate.viewContext.fetch(request) else {
return []
}
return tasks
}
static func deleteAll(viewContext: NSManagedObjectContext = AppDelegate.viewContext) {Task.fetchAll(viewContext: viewContext).forEach({ viewContext.delete($0) })try? viewContext.save()}

We added two static methods here:

  • fetchAll() : to fetch all data and return an Array of Task type
  • deleteAll() : to delete all stored tasks

Deleting a task from the persistent store involves three steps:

  1. Fetch the tasks that need to be deleted
  2. Delete the tasks
  3. Save the changes in the context

6. Save task in local storage

Since our model is ready to use, we can save our tasks in Core Data.

In ViewController.swift:

Add those properties:

var tasksList = Task.fetchAll()var enteredTask = “”

taskList is an array of Task type stored in Core Data that we will use in the table view.

Add UITableViewDataSource extension to ViewController:

extension ViewController: UITableViewDataSource {func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {return tasksList.count}func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {let cell = UITableViewCell(style: .default, reuseIdentifier: “taskCell”)cell.textLabel?.text = “- “ + tasksList[indexPath.row].taskName!return cell}}

For the cell reuseIdentifier, give the name of the prototype cell you entered in the main.storyboard.

Here I created 3 methods, one to add a new task, one to save this task in CoreData, and the last to delete all saved tasks.

@IBAction func addButtonTapped(_ sender: UIButton) {addNewTask()}@IBAction func resetButtonTapped(_ sender: UIBarButtonItem) {deleteAllTask()}
private func addNewTask() {self.enteredTask = taskTextField.text!saveTask(named: self.enteredTask)self.tasksList = Task.fetchAll()tableView.reloadData()taskTextField.text = “”}private func saveTask(named name: String) {let task = Task(context: AppDelegate.viewContext)task.taskName = nametry? AppDelegate.viewContext.save()}private func deleteAllTask() {Task.deleteAll()tasksList = Task.fetchAll()tableView.reloadData()}}
ViewController.swift file

7. Adding capabilities

The last step is to enable iCloud. For that, you need to go to your Xcode project settings. Then select Signing & Capabilities.

. Enable iCloud

Enable iCloud capability

Click on (+) Capability button and write “iCloud” then double click on it.

Select “CloudKit” checkbox then click on (+) button in Containers.

/!\ Before adding a new container ID, you need to know that you can’t delete a container ID. It will stay on your CloudKit Dashboard. /!\

In the dialog that appears, enter an identifier for the container you want to add. I copied and pasted my Bundle identifier.

. Enable Background modes

Enable Background modes capability

Click again on (+) button and write” background modes”, double click to add. Then select the Remote notifications checkbox. This allows the CloudKit persistence to update in the background.

. viewContext.automaticallyMergesChangesFromParent

One more thing to do is to add this code in the AppDelegate: viewContext.automaticallyMergesChangesFromParent = true

I modified the view context property to this:

static var viewContext: NSManagedObjectContext {let viewContext = persistentContainer.viewContextviewContext.automaticallyMergesChangesFromParent = truereturn viewContext}

Your app can now store data in iCloud.

. CloudKit DashBoard

In Signing & Capabilities, click on the CloudKit Dashboard button:

Once in the CloudKit Dashboard, you can visualize the created container on the left menu:

CloudKit Dashboard

If you select “Schema” in CloudKit dashboard, you can see that the Entity created in Core Data is automatically created on CloudKit!

Schema — CloudKit Dashboard

8. Result

Launch two simulators with Xcode: one iPhone and one iPad.

Go to settings and sign in with your Apple ID on both devices:

Sign in Apple ID in iPad launched with the simulator

You can see the synchronization between those two devices in the video below:

Data sync between iPhone and iPad connected to the cloud

That’s it! Let me know if something didn’t work right for you, or if this tutorial was helpful!

You can get the source code from this tutorial on my GitHub.

Thank you for reading and happy coding! 👩🏻‍💻

--

--