CloudKit With SwiftUI
CloudKit With SwiftUI
for Masterminds
CloudKit
with SwiftUI
Learn how to share data between devices with CloudKit databases
J.D Gauchat
www.jdgauchat.com
Quick Guides for Masterminds
Copyright © 2022 John D Gauchat
All Rights Reserved
Apple™, iPhone™, iPad™, Mac™, among others mentioned in this work, are
trademarks of Apple Inc.
ICLOUD
CLOUDKIT
ENABLING CLOUDKIT
IMPLEMENTING CLOUDKIT
CUSTOM IMPLEMENTATION
RECORDS
ZONES
QUERY
ASYNCHRONOUS OPERATIONS
BATCH OPERATIONS
REFERENCES
CLOUDKIT DASHBOARD
CUSTOM CLOUDKIT APPLICATION
ASSETS
SUBSCRIPTIONS
ERRORS
DEPLOY TO PRODUCTION
ICLOUD
Data in the Cloud
These days, users own more than one device. If we create an application
that works on multiple devices, we must provide a way for the users to
share the data, otherwise they will have to insert the same information on
every device they own. But the only way to do it effectively is through a
server. Data from one device is stored on a server so that it can be
retrieved later from other devices. Setting up a server to run this kind of
systems is complicated and costly. To provide a standard solution, Apple
created a free system called iCloud. iCloud allows applications to
synchronize data across devices using Apple servers. The system includes
three basic services: Key-Value Storage, to store single values, Document
Storage, to store files, and CloudKit Storage, to store structured data in
public and private databases.
Enabling iCloud
After iCloud is added to the app, it is shown on the panel, below the
signing section. From here, we can select the services we want to activate
and Xcode takes care of creating the entitlements. Figure 2, below, shows
the panel with all the options available.
The best way to test iCloud is by running the app in two different devices,
but Apple has made iCloud services available in the simulator as well.
Thanks to this feature, we can synchronize data between a device and the
simulator to test our app.
For the devices and the simulators to be able to access iCloud services, we
must register our iCloud account in the Settings app. We have to go to the
Home screen, access the Settings app, tap on the option Sign in to your
iPhone/iPad (Figure 3, center), and sign in to Apple services with our Apple
ID. We must repeat the process for every device or simulator we want to
use.
The first step to use CloudKit is to activate it from the Signing & Capabilities
panel.
And the Countries entity needs an attribute of type String called name and
a To-Many relationship called cities, as shown below.
Figure 9: Countries entity
There is one more requirement for the model to be ready to work with
CloudKit. We must select the Configuration (Figure 10, number 1) and
check the option Used with CloudKit in the Data Model Inspector panel
(Figure 10, number 2). This makes sure that if we create other
configurations later, the system knows which one must be synchronized
with CloudKit servers.
Do It Yourself: Create a Core Data model from the File menu. Add
two entities to the model called Countries and Cities with their
respective attributes and relationships, as shown in Figures 8 and 9.
Select the Default configuration (Figure 10, number 1), open the
Data Model Inspector panel on the right, and check the Used with
CloudKit option (Figure 10, number 2).
IMPORTANT: If you later want to create additional Entities to store
information only on the device, you can add a new configuration to
the Core Data model, assign the Entities to that configuration, and
keep the Used with CloudKit option unchecked. All the objects
stored for those Entities will not be synchronized with CloudKit.
Once the application is configured to work with CloudKit and the Core Data
model is ready, we can work on our code. First, we must initialize the Core
Data stack using the NSPersistentCloudKitContainer class.
@main
struct TestApp: App {
@StateObject var appData = ApplicationData()
And that's all it takes. From now on, every change introduced in the
Persistent Store is going to be uploaded to CloudKit and every device
running the application is going to be automatically synchronized. All that
is left is to design the views to display the objects or add new ones. Here is
the initial view for our example.
There is nothing new in this view. We create a new Countries object with the
value inserted by the user when the Save button is pressed and save the
context with the save() method, but because the application is connected to
CloudKit the system automatically creates a record from the Countries object
and uploads it to CloudKit servers.
The following is the ShowsCitiesView view opened when a country is selected
by the user.
Listing 5: Listing the cities stored in the Persistent Store

import SwiftUI
import CoreData
init(selectedCountry: Countries?) {
self.selectedCountry = selectedCountry
if selectedCountry != nil {
_listCities = FetchRequest(sortDescriptors: [SortDescriptor(\Cities.name, order:
.forward)], predicate: NSPredicate(format: "country = %@", selectedCountry!), animation:
.default)
}
}
var body: some View {
List {
ForEach(listCities) { city in
Text(city.name ?? "Undefined")
}
}
.navigationBarTitle(selectedCountry?.name ?? "Undefined")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Add City") {
openSheet = true
}
}
}
.sheet(isPresented: $openSheet) {
InsertCityView(country: selectedCountry)
}
}
}
struct ShowCitiesView_Previews: PreviewProvider {
static var previews: some View {
NavigationStack {
ShowCitiesView(selectedCountry: nil)
.environment(\.managedObjectContext, ApplicationData.preview.container.viewContext)
}
}
}

This view lists the cities with a List view, as we did before for the countries,
but because we only need to show the cities that belong to the selected
country, we initialize the @FetchRequest property wrapper with a false
predicate and then use the Countries object returned by the selectedCountry
property to create a new FetchRequest structure with a predicate that filters
the cities by country.
The ShowCitiesView view also includes a button to let the user insert new
cities. The following is the view opened by the sheet() modifier when the
button is pressed.
Again, we just create a new Cities object with the value inserted by the user
when the Save button is pressed and the system takes care of creating the
record an uploading it to CloudKit.
The application shows the list of countries and cities stored in the
Persistent Store, and allows the user to insert new values, as previous Core
Data applications, but now all the information is uploaded to CloudKit
servers and automatically shared with other devices.

Do It Yourself: Create a Swift file called ApplicationData.swift for the
model in Listing 1. Remember to assign the name of your Core Data
model to the NSPersistentCloudKitContainer initializer. Update the App
structure with the code in Listing 2 and the ContentView.swift file
with the code in Listing 3. Create SwiftUI View files called
InsertCountryView.swift, ShowCitiesView.swift, and
InsertCityView.swift for the codes in Listings 4, 5, and 6. Run the
application in two devices. Press the Add Country button and insert a
new country. After a few seconds, you should see the same value
appear on the second device. Repeat the process for the cities.
Custom Implementation
The CKRecord class offers properties to set or get the record's ID and other
attributes. The following are the most frequently used.
Because the values of a record are stored as key/value pairs, we can use
square brackets to read and modify them (as we do with dictionaries), but
the class also includes the following methods.
CloudKit is an online service and therefore any task may take time to
process. For this reason, the CloudKit framework uses asynchronous
operations to access the information on the servers. An operation must be
created for every process we want to perform on a database, including
storing, reading, and organizing records.
These operations are like Swift asynchronous tasks but created from
classes defined in the Foundation framework. The CloudKit framework
includes its own subclasses of the Foundation classes to define operations.
There is a base class called CKDatabaseOperation, and then several subclasses
for every operation we need. Once an operation is defined, it must be
added to the CKDatabase object that represents the database we want to
modify. The CKDatabase class offers the following method for this purpose.
Although we can create single operations and assign them to the database,
as we will see later, the CKDatabase class also offers convenient methods to
generate and execute the most common. The following are the methods
available to process records.
The following are the methods provided by the CKDatabase class to process
zones.
Although the methods provided by the CKDatabase class are very convenient
and easy to implement, they only perform one request at a time. The
problem is that CloudKit servers have a limit on the number of operations
we can perform per second (currently 40 requests per second are allowed),
so if our application relies heavily on CloudKit, at one point some of the
requests might be rejected if we send them one by one. The solution is to
create operations that allow us to perform multiple requests at once. The
CloudKit framework defines three subclasses of the CKDatabaseOperation
class for this purpose. The CKModifySubscriptionsOperation class creates an
operation to add or modify subscriptions, the CKModifyRecordZonesOperation
class is used to add or modify record zones, and the CKModifyRecordsOperation
class is for adding and modifying records.
CKModifySubscriptionsOperation(subscriptionsToSave:
[CKSubscription], subscriptionIDsToDelete:
[CKSubscription.ID])—This initializer returns an operation that adds
or modifies one or more subscriptions. The subscriptionsToSave
argument is an array with the subscriptions we want to add or modify,
and the subscriptionIDsToDelete argument is an array with the IDs of
the subscriptions we want to delete from the server.
CKModifyRecordZonesOperation(recordZonesToSave:
[CKRecordZone], recordZoneIDsToDelete: [CKRecordZone.ID])
—This initializer returns an operation that adds or modifies one or
more record zones. The recordZonesToSave argument is an array with
the record zones we want to add or modify, and the
recordZoneIDsToDelete is an array with the IDs of the record zones
we want to delete from the server.
CKModifyRecordsOperation(recordsToSave: [CKRecord],
recordIDsToDelete: [CKRecord.ID])—This operation adds or
modifies one or more records. The recordsToSave argument is an
array with the records we want to add or modify, and the
recordIDsToDelete argument is an array with the IDs of the records
we want to delete from the server.
These operations must be initialized first and then added to the database
with the add() method of the CKDatabase class. Each initializer offers the
options to modify elements and remove them. If we only need to perform
one task, the other can be omitted with the value nil.
References
Records of different types are usually related. For example, along with
records of type Countries we may have records of type Cities to store
information about the cities of each country. To create these relationships,
records include references. References are objects that store information
about a connection between one record and another. They are created
from the Reference class defined inside the CKRecord class. The following are
its initializers.

CloudKit Dashboard

If we click on the CloudKit Database button, a panel is loaded to edit the
database. The panel includes an option at the top to select the container
(Figure 14, number 1), a bar on the left to edit the data and the schema,
and a bar on the right to show and edit the values.
The bar on the left includes a button to select from two configurations:
Development and Production (Figure 14, number 2). The Development
option shows the configuration of the database used during development.
This is the database we use as we develop our app. The Production option
shows the configuration of the database that we are going to deliver with
our app (the one that is going to be available to our users).
During development, we can store information in the database for testing.
Below the configuration option is the Data section (Figure 14, number 3)
where we can edit the data stored by our application, including records,
zones, and subscriptions. The panel also offers an option on the right to
add records (Figure 14, number 4), and buttons to select the database we
want to access (Public, Private, or Shared), select the zone, and indicate
how we want to access the records.
From the Record Types option, we can add, modify, or delete record types
(Entities) and their values (Attributes). The option to add a new record type
is at the top of the panel (Figure 16, number 1), and the record types
already created are listed below (Figure 16, number 2).

Custom CloudKit Application
Using the tools introduced above, we can implement CloudKit on our own,
but there are several ways to do it. It all depends on the characteristics of
our application and what we want to achieve. An alternative is to centralize
all the logic in the model. For instance, the following is a possible
implementation of the application created before with Core Data to store
countries and cities.
struct Country {
var name: String?
var record: CKRecord
}
struct City {
var name: String?
var record: CKRecord
}
struct CountryViewModel: Identifiable {
var id: CKRecord.ID
var country: Country
Task(priority: .high) {
await readCountries()
}
}
func insertCountry(name: String) async {
let id = CKRecord.ID(recordName: "idcountry-\(UUID())")
let record = CKRecord(recordType: "Countries", recordID: id)
record.setObject(name as NSString, forKey: "name")
do {
try await database.save(record)
await MainActor.run {
let newCountry = Country(name: record["name"], record: record)
let newItem = CountryViewModel(id: record.recordID, country: newCountry)
listCountries.append(newItem)
listCountries.sort(by: { $0.countryName < $1.countryName })
}
} catch {
print("Error: \(error)")
}
}
func insertCity(name: String, country: CKRecord.ID) async {
let id = CKRecord.ID(recordName: "idcity-\(UUID())")
let record = CKRecord(recordType: "Cities", recordID: id)
record.setObject(name as NSString, forKey: "name")
do {
try await database.save(record)
await MainActor.run {
let newCity = City(name: record["name"], record: record)
let newItem = CityViewModel(id: record.recordID, city: newCity)
listCities.append(newItem)
listCities.sort(by: { $0.cityName < $1.cityName })
}
} catch {
print("Error: \(error)")
}
}
func readCountries() async {
let predicate = NSPredicate(format: "TRUEPREDICATE")
let query = CKQuery(recordType: "Countries", predicate: predicate)
do {
let list = try await database.records(matching: query, inZoneWith: nil, desiredKeys: nil,
resultsLimit: 0)
await MainActor.run {
listCountries = []
for (_, result) in list.matchResults {
if let record = try? result.get() {
let newCountry = Country(name: record["name"], record: record)
let newItem = CountryViewModel(id: record.recordID, country: newCountry)
listCountries.append(newItem)
}
}
listCountries.sort(by: { $0.countryName < $1.countryName })
}
} catch {
print("Error: \(error)")
}
}
func readCities(country: CKRecord.ID) async {
let predicate = NSPredicate(format: "country = %@", country)
let query = CKQuery(recordType: "Cities", predicate: predicate)
do {
let list = try await database.records(matching: query, inZoneWith: nil, desiredKeys: nil,
resultsLimit: 0)
await MainActor.run {
listCities = []
for (_, result) in list.matchResults {
if let record = try? result.get() {
let newCity = City(name: record["name"], record: record)
let newItem = CityViewModel(id: record.recordID, city: newCity)
listCities.append(newItem)
}
}
listCities.sort(by: { $0.cityName < $1.cityName })
}
} catch {
print("Error: \(error)")
}
}
}

We begin by defining two structures, Country and City, to store the name of
the country and city, and also a reference to the record downloaded from
the CloudKit database, and two more for our view model, CountryViewModel
and CityViewModel. These view models identify each value by the record ID
(CKRecord.ID) and include a computed property to return a string with the
name.
The observable object defines the @Published properties we need to store
the data locally and show it to the user. The listCountries property is an array
with the list of countries already inserted in the database, and the listCities
property is another array with the list of cities available for a specific
country. Another property included in this class is database. This property
stores a reference to the CloudKit's database, so we can access it from
anywhere in the code. The property is initialized in the init() method with a
reference to the Private Database. (We use the private database because
we only want the user to be able to share the data between his or her own
devices.)
The observable object also includes methods to add and read records. For
instance, the insertCountry() and insertCity() methods are going to be called
from the views when the user inserts a new country or city. Their task is to
create the records and upload them to CloudKit. The process begins by
defining a record ID, which is a unique value that identifies each record. For
the countries, we use the string "idcountry" followed by a random value
generated by the UUID() function. Using this ID, we create a CKRecord object
of type Countries, then add a property called "name" with the value
received by the method, and finally save it in CloudKit servers with the
save() method of the CKDatabase object. This method generates an operation
that communicates with the servers asynchronously. If there is no error, we
add the record to the listCountries property, which updates the views and the
screen.
The method to add a city is the same, with the exceptions that we must
define the type of records as Cities and add an extra attribute to the record
with a reference to the country the city belongs to. For this purpose, we
create a Reference object with the country's record ID received by the
method and an action of type deleteSelf, so when the record of the country is
deleted, this record is deleted as well.
Next are the methods we need to implement to read the countries and
cities already stored in the database. In the readCountries() method, we
define a predicate with the TRUEPREDICATE keyword and a query for
records of type Countries. The record type asks the server to only look for
records of type Countries, and the TRUEPREDICATE keyword determines
that the predicate will always return true, so we get back all the records
available. If the query doesn't return any errors, we get the records from
the matchResults value and add them to the listCountries array to update the
views.
The readCities() method is very similar, except that this time we are getting
the list of cities that belong to the country selected by the user. (The view
that shows the cities only opens when the user taps on a row to select a
country.) The rest of the process is the same. We get the records that
represent the cities, create the CityViewModel structures with them, and
store them in the listCities array.
For the interface, we need a total of four views: a view to show the list of
countries, a view to allow the user to insert a new country, a view to show
the list of cities that belong to the selected country, and another view to
allow the user to insert a new city. The following is the initial view.
Task(priority: .high) {
await appData.insertCountry(name: text)
dismiss()
}
}
}.disabled(buttonDisabled)
}
Spacer()
}.padding()
}
}
struct InsertCountryView_Previews: PreviewProvider {
static var previews: some View {
InsertCountryView().environmentObject(ApplicationData())
}
}

This view includes a TextField view to insert the name of the country and a
button to save it in the database. When the button is pressed, we call the
insertCountry() method in the model. The method creates the record with the
value inserted by the user and calls the save() method on the database to
store it in CloudKit servers.
Next is the view necessary to show the list of cities available for each
country. The view is called ShowCitiesView and opens when the user taps on a
row in the initial view to select a country.
Task(priority: .high) {
await appData.insertCity(name: text, country: self.country)
dismiss()
}
}
}.disabled(buttonDisabled)
}
Spacer()
}.padding()
}
}
struct InsertCityView_Previews: PreviewProvider {
static var previews: some View {
InsertCityView(country: CKRecord.ID(recordName: "Test"))
.environmentObject(ApplicationData())
}
}

The view receives the country's record ID to know which country the city
belongs to, and then calls the insertCity() method in the model with the
name inserted by the user and this ID to create a Cities record connected
to that Countries record in the CloudKit database.
The application is ready. When the user inserts a new value, the code
creates a record and uploads it to CloudKit servers, but if we stop and start
the application again, the countries are not shown on the screen anymore.
This is because we haven't defined the required indexes.
CloudKit automatically creates indexes for every key we include in the
records, except for the record’s identifier. Therefore, when we query the
Cities records by their country attribute to get the cities that belong to the
country selected by the user, CloudKit knows how to find and return those
records, but when we try to retrieve the Countries records without a
predicate, CloudKit tries to fetch them by the record identifiers and fails
because there is no index associated to that attribute (called recordName).
To create an index for this attribute, we need to go to the dashboard, click
on the Indexes option in the Schema section, and click on the Countries
record type to modify it.
When we click on a record, the panel shows the list of indexes available. By
default, the Countries records contains three indexes for the name
attribute, but no index for the record's identifier.
To add an index, we must press the Add Basic Index button at the bottom
of the list. The panel opens a form to create a new index.
Figure 18: Index configuration
There are three types of indexes: Queryable (it can be included in a query),
Searchable (it can be searched), and Sortable (it can be sorted). By default,
all these indexes are associated to custom attributes but not to the record's
identifier. When we query the database from the readCountries() method in
the model, we do not specify any field in the predicate and therefore the
system fetches the records by their identifiers, which is described in the
database as recordID (recordName). For this reason, to retrieve the
countries in our example, we must add a Queryable index to the
recordName field of the Countries record type, as shown in figure 18.
Once we select the recordName field and the Queryable index, we can
press the Save button to save the changes. Now, if we run the application
again from Xcode, the records added to the database are shown on the
screen.
Records may also include files, and the files may contain anything from
pictures to sound or even videos. To add a file to a record, we must create
an asset with the CKAsset class. The class includes the following initializer.
The assets are added to a record with a key, as any other value. For our
example, we are going to store a picture in the record of every city and add
a view to show the picture when the city is selected.
In the new insertCity() method introduced in Listing 12, we get the URL of an
image included in the project called Toronto, create a CKAsset object with it,
and assign the object to the record of every city with the "picture" key.
Now, besides the name, a file with this image will be stored for every city
inserted by the user. To get the asset back, we must add a computed
property to the view model, as shown next.
How to read the assets stored in the record depends on the type of
content managed by the asset. In this example, we must use the asset's
URL to create a UIImage object to show the image to the user. For this
purpose, the cityPicture property introduced in Listing 13 reads the value in
the record with the "picture" key, casts it as a CKAsset object, and gets the
asset's URL from the fileURL property. If the process is successful, we create
a UIImage object with the image in this URL and return it, otherwise, we
return a UIImage object with a placeholder image (nopicture).
The following are the modifications we must introduce to the ShowCitiesView
view to provide the NavigationLink view required for the user to be able to
select a city and open a view to see the city's picture.
This view receives the information about the selected city through the
selectedCity property. From this property, we get the city's picture and name
and show them to the user. As a result, every time the user selects a city,
the asset is turned into an image and displayed on the screen.
The previous example fetches the records available and shows them on the
screen every time the app is launched. This means that records added to
the database from another device will not be visible until the app is
launched again. This is not the behavior expected by the user. When
working with applications that store information online, users expects the
information to be updated as soon as it becomes available. To provide this
feature, CloudKit uses subscriptions.
Subscriptions are queries stored by our application in CloudKit servers.
When a change occurs in the database, the query detects the modification
and triggers the delivery of a Remote Notification from the iCloud servers
to the copy of the app that registered the subscription.
Database subscriptions are created from the CKDatabaseSubscription class (a
subclass of a generic class called CKSubscription). The class includes the
following initializer.
application(UIApplication, didReceiveRemoteNotification:
Dictionary, fetchCompletionHandler: Block)—This method is
called by the application on its delegate when a Remote Notification is
received. The didReceiveRemoteNotification argument is a dictionary
with information about the notification, and the
fetchCompletionHandler argument is a closure that we must execute
after all the custom tasks are performed. The closure must be called
with a value that describes the result of the operation. For this
purpose, UIKit offers the UIBackgroundFetchResult enumeration with the
values newData (new data was downloaded), noData (no data was
downloaded), and failed (the app failed to download the data).
CKNotification(fromRemoteNotificationDictionary:
Dictionary)—This initializer creates a CKNotification object from the
information included in the notification (the value of the userInfo
parameter in the delegate method).
@main
struct TestApp: App {
@UIApplicationDelegateAdaptor(CustomAppDelegate.self) var appDelegate
@StateObject var appData = ApplicationData.shared
Listing 18: Defining the properties to control the subscription and the
custom zone

class ApplicationData: ObservableObject {
@AppStorage("subscriptionSaved") var subscriptionSaved: Bool = false
@AppStorage("zoneCreated") var zoneCreated: Bool = false
@AppStorage("databaseToken") var databaseToken: Data = Data()
@AppStorage("zoneToken") var zoneToken: Data = Data()
private init() {
let container = CKContainer.default()
database = container.privateCloudDatabase
Task(priority: .high) {
await readCountries()
}
}

do {
try await database.save(newSubscription)
await MainActor.run {
subscriptionSaved = true
}
} catch {
print("Error: \(error)")
}
}
if !zoneCreated {
let newZone = CKRecordZone(zoneName: "listPlaces")
do {
try await database.save(newZone)
await MainActor.run {
zoneCreated = true
}
} catch {
print("Error: \(error)")
}
}
}

The first thing we do in the configureDatabase() method is to check the value
of the subscriptionSaved property to know if a subscription was already
created. If not, we use the CKDatabaseSubscription initializer to create a
subscription with the name "updatesDatabase" and then define the
notificationInfo property to configure the notifications that are going to be
sent by the server. For this purpose, the framework defines the
CKNotificationInfo class. This class includes multiple properties to configure
Remote Notifications for CloudKit, but database subscriptions only require
us to set the shouldSendContentAvailable property with the value true. After this,
the subscription is saved on the server with the save() method of the
CKDatabase object and the value true is assigned to the subscriptionSaved
property if the operation is successful.
We follow the same procedure to create the custom zone. We check the
value of the zoneCreated property to know if there is a custom zone already
on the server, and if not, we create one called "listPlaces" to store our
records. If the operation is successful, we assign the value true to the
zoneCreated property to indicate that the zone was already created.
Next, we must define the checkUpdates() method to download and process
the changes in the database. But first, we need to think about how we are
going to organize our code. Every process executed in CloudKit servers is
performed by asynchronous operations. This means that we need to think
about the order in which the operations are executed. For some, the order
doesn't matter, but for others it is crucial. For instance, we cannot store
records in a zone before the zone is created. We could use Swift
concurrency to control the order in which tasks are performed, but
CloudKit operations are already concurrent. Therefore, depending on the
requirements of our application, it may be better to just implement
closures that execute code after the operations are over, and this is the
approach we take in this example.
The procedure is as follows. Every time we call the checkUpdates() method to
download the data, we send a closure to the method with the code we
want to execute once the operation is over. This way, we make sure that
the operations are over before doing anything else.
Listing 20: Initiating the process to get the updates from the server

func checkUpdates(finishClosure: @escaping (UIBackgroundFetchResult) -> Void) {
Task(priority: .high) {
await configureDatabase()
downloadUpdates(finishClosure: finishClosure)
}
}

CKFetchDatabaseChangesOperation(previousServerChangeT
oken: CKServerChangeToken?)—This initializer creates an
operation to fetch changes from a database. The argument is a token
that determines which changes were already fetched. If we specify a
token, only the changes that occurred after the token was created are
fetched.
CKFetchRecordZoneChangesOperation(recordZoneIDs:
[CKRecordZone.ID], configurationsByRecordZoneID:
Dictionary)—This initializer creates an operation to download
changes from a database. The recordZoneIDs argument is an array
with the IDs of all the zones that present changes, and the
configurationsByRecordZoneID argument is a dictionary with
configuration values for each zone. The dictionary takes
CKRecordZone.ID objects as keys and options determined by an object
of the ZoneConfiguration class included in the
CKFetchRecordZoneChangesOperation class. The class includes three
properties to define the options: desiredKeys (array of strings with the
keys we want to retrieve), previousServerChangeToken (CKServerChangeToken
object with the current token), and resultsLimit (integer that determines
the number of records to retrieve).
CloudKit servers use tokens to know which changes were already sent to
every instance of the app, so the information is not downloaded twice
from the same device. If a device stores or modifies a record, the server
generates a new token, so the next time a device accesses the servers only
the changes introduced after the last token was created will be
downloaded.
In the process depicted in Figure 20, the app in Device 1 stores a new
record in the server (Record 1). To report the changes, the server generates
a new token (A). When the app in Device 2 connects to the server, the
server detects that this device does not have the latest token, so it returns
Record 1 and the current token (A) to update the state in this device. If
later the user decides to create a new record from Device 2 (Record 2), a
new token will be created (B). The next time Device 1 connects to the
server, it will find that its token is different from the server's token, so it
will download the modifications inserted after token A.
Tokens are great because they allow us to only get the latest changes, but
this process is not automatic, we are responsible of storing the current
tokens and preserve the state of our app. The server creates a token for
the database and a token for each of the custom zones. For our example,
we need two tokens: one to keep track of the changes in the database and
another for the custom zone created by the configureDatabase() method. To
work with these values, we are going to use two variables called
changeToken, for the database token, and fetchChangeToken, for the token of
our custom zone, and we are going to store them permanently with the
@AppStorage properties defined before in the model (databaseToken and
zoneToken). All this process is performed by the downloadUpdates() method.
do {
try await database.save(record)
await MainActor.run {
let newCountry = Country(name: record["name"], record: record)
let newItem = CountryViewModel(id: record.recordID, country: newCountry)
listCountries.append(newItem)
listCountries.sort(by: { $0.countryName < $1.countryName })
}
} catch {
print("Error: \(error)")
}
}
}
func insertCity(name: String, country: CKRecord.ID) async {
await configureDatabase()
If the status of the iCloud account changes while the app is running, the
system posts a notification that we can use to perform updates and
synchronization tasks.
do {
let container = CKContainer.default()
let status = try await container.accountStatus()
if status != CKAccountStatus.available {
print("iCloud Not Available")
return
}
} catch {
print("Error: \(error)")
return
}
let text = name.trimmingCharacters(in: .whitespaces)
if !text.isEmpty {
let zone = CKRecordZone(zoneName: "listPlaces")
let id = CKRecord.ID(recordName: "idcountry-\(UUID())", zoneID: zone.zoneID)
let record = CKRecord(recordType: "Countries", recordID: id)
record.setObject(text as NSString, forKey: "name")
do {
try await database.save(record)
await MainActor.run {
let newCountry = Country(name: record["name"], record: record)
let newItem = CountryViewModel(id: record.recordID, country: newCountry)
listCountries.append(newItem)
listCountries.sort(by: { $0.countryName < $1.countryName })
}
} catch {
print("Error: \(error)")
}
}
}

This example checks the status of the account and prints a message on the
console if an error occurs or the status is other than available. If an error
occurs, the code returns from the function without letting the user insert
the new record.
In the last example, we just checked whether an error occurred or not and
proceeded accordingly, but we can also identify the type of error returned
by the operation. Errors are structures that conform to the Error protocol.
Every time we want to read an error, we must cast it to the right type. In
CloudKit, the errors are of type CKError, a structure that includes the
following property to return the error code.
J.D Gauchat
www.jdgauchat.com