Data Persistence with SwiftData

Mar 19 2025 · Swift 5.10, iOS 17, ipadOS 17, macOS 15, visionOS 1.2, Xcode 15

Lesson 04: Extending SwiftData Apps & CloudKit Support

CloudKit Support & Extending SwiftData Apps Demo

Episode complete

Play next episode

Next
Transcript

Extending SwiftData Apps & CloudKit

Enabling CloudKit

You can start with the app you started building in the previous lesson, or you can start with the app in the Starter folder for this lesson. You’ll need to use a real Apple ID and developer account to enable CloudKit. Currently, SwiftData can only work with a CloudKit Private Database.

During development, Apple recommends creating and using sandbox Apple IDs. Apple has documentation on creating Sandbox accounts. Log into your developer account’s Users and Access to create a test account with an email address that hasn’t been used to make an Apple ID.

Once you have a sandbox account, you can add it to your devices in the App Store section in Settings. Later, you’ll see that checking data entries in the CloudKit Console is better with a separate account.

To enable CloudKit in your app, select the GoodDog project file in the Project Navigator. To start, you need to set a Bundle Identifier with a reverse domain name appended with your app name, and choose a developer certificate.

Team: Kodeco LLC.

Bundle Identifier: com.kodeco.GoodDog

Adding Capabilities

Next, select Signing & Capabilities, and click the + Capability. Look for iCloud and double-click it to add to your capabilities. Check the box beside CloudKit.

[x] CloudKit

Next, add Background Modes to your app. Select Signing & Capabilities, and click the + Capability. Look for Background Modes in the search field. Double-click to add it. Check the box beside Remote Notifications.

[x] Remote Notifications

Go to the iCloud section. Click the + in Containers and enter a name for a new CloudKit Container. Name the new container with iCloud.your-reverse-domain.GoodDog. By using your own reverse domain you assure that the container name is unique.

iCloud.com.kodeco.GoodDog

Once you’ve completed these steps, you’ll see a GoodDog.entitlements file added to the Project Navigator. It’s a plist file in xml format. You can view it in Xcode, where you’ll see your container name in the iCloud Container Identifiers. It’s possible to have multiple containers, but for this app you’ll only use one.

Updating Models for CloudKit

You now need to modify the models before you run the app. CloudKit requires that all non-optional properties have a default value. Also, CloudKit doesn’t support the unique attribute.

Open the DogModel, and give the name a default empty string.

var name: String = ""

The rest of the properties are optional types, so they’re good.

Edit the ParkModel, and give the park.name a default empty string.

var name: String = ""

For the BreedModel, you give the name a default Unknown Breed string value and remove the unique attribute.

/* @Attribute(.unique) */
var name: String = "Unknown Breed"

Recall that you added logic to create a default Unknown Breed when creating a new record.

Now, build and run the app on the Simulator. If the application does break, it will stop on the Fatal Error in the catch when it tries to make the ModelContainer. Check the information in the debug console for possible causes.

Log in with an Apple ID if you’re running on the Simulator or a device in the Settings app. You can use the sandbox account you created earlier. Create some dogs, breeds and parks if you have no current pet records.

Note: If you see an error in the Debug Console, such as, "Couldn't get container configuration from the server for container "iCloud.com.kodeco.GoodDog"". This might happen on the first run. Stop the app and build and run again. CloudKit hasn’t set up the container yet.

If you have some dog records and you’re logged in with an Apple ID, the Debug Console should be busy with messages. When you see a message like the following, it means that the CloudKit container has been synced with your device - <NSCloudKitMirroringDelegate: 0x600003d08690>: No more requests to execute.

Pro Tip: The Simulator will sync with CloudKit between simulated devices. However if you want to see the CKRecords in the CloudKit Console you may need to run the app on an actual device while logged in to iCloud.

CloudKit Console

You can get to the CloudKit Console from the Signing & Capabilities pane on your app’s target. In the iCloud section, click the button that says CloudKit Console. Log in with your Apple developer account. Then, if you belong to more than one development team, choose the Team on the top-right of the window.

Select the GoodDogs CloudKit container you made from the pulldown on the top-left of the dashboard. The name matches the name you gave it in Xcode. Recall that SwiftData currently uses Private Database. Choose Private Database under Records in the center of the window.

New CloudKit Console Features

You have the option to view Telemetry, Notifications, view Logs, and set up Alerts in the 2024 console updates. These are beyond the scope of the course, but you can tap the bell icon to see notifications. Telemetry shows requests, errors, latency, and bandwidth metrics. In Logs, you can see and export specific event information.

Setting Up Queryable Tables

From the Record Type pulldown menu, you should see CDMR, CD_BreedModel, CD_DogModel, CD_ParkModel, and Users in the menu. Click Query Records. You won’t be able to see any records for two possible reasons. You need to enable the table to be queried, and you need to use a sandbox user to log in to iCloud on your Simulator or device. There may be an error saying - ‘Field recordName is not marked queryable’ if this is a new container. You’ll fix that now.

On the left side, click Record Types under the Schema heading. Click CD_DogModel under Record Types. In the Metadata on the right, locate recordName, and notice that it has None in the Single Field Indexes column.

Go back to Records. On the left side, click Indexes under the Schema heading. Click the + to add an index. In the modal, choose CD_DogModel in the Record Type picker. Enter the name ___recordID in the Name field. Choose Queryable from the Type picker. This will reveal a Field picker. Select recordName and click the Add button.

Check the recordName from Schema, Record Types, CD_DogModel and note that recordName is now Queryable.

Under Data, choose Records from the left sidebar. You are now back to the default view in the dashboard. Choose Private Database from the pulldown and select CD_DogModel. Now, clicking the Query Records will work, however, there should be no records, since you’re looking at your own developer account’s records.

Viewing Sandbox Data

To view your sandbox user’s records, locate Act As iCloud Account… on the left. You might need to scroll down. In the iCloud Account Sign In modal, click the Open Sign in Window button. Note that the login says Sign in to iCloud.com.[yourdomain].GoodDog. Enter the sandbox user email and password. When you’re successfully logged in, you’ll see an Acting as iCloud Account modal. Click Continue. At the top of the dashboard, you’ll see Acting as iCloud Account “_b6319e434154960e05978c1e1ad6b397”.

Choose Private Database, and check the zones pulldown, which may say __defaultZone. If you don’t see another choice, go to Zones under Data on the left. At the top, click Fetch All Zones to update the zones. Now you should see com.apple.coredata.cloudkit.zone.

Go back to Records under Data. Select Private DataBase and com.apple.coredata.cloudkit.zone. Now choose CD_DogModel from the Record Type. Finally, click the blue Query Records button on the right.

Displayed in the center, you’ll see the dog records you created in your app. On the left is a unique CloudKit Name for the record, followed by the Type, and the fields used in the app’s model are prefixed with CD_. If you add a breed value, you’ll see the unique CloudKit Name for the CD_breed value.

Click on a record’s Name link and on the right side, you’ll see a scrollable form. This is an interface where you can create or modify some of the fields, but that’s beyond the scope of this course. Changes made here should sync to your apps, if you’re careful.

Syncing Between Devices

Now that you’ve seen the data on CloudKit, you can build and run on another device model in the Simulator or on another device. The dog records data will sync after the app runs, and you’ll log in with the same Apple ID. It takes a few seconds to sync between devices as SwiftData and CloudKit do the heavy lifting.

Preparing macOS Image Support

Before running the app on your Mac, you need to prepare to support images. Recall that the mock data uses UIKit for images, but macOS doesn’t support UIImage. This can be solved with typeAlias to treat UIImage as NSImage when the app is run on macOS.

Select DogList.swift from the Project Navigator. At the top of the file, import Cocoa add the typeAlias:

#if os(macOS)
import Cocoa
typealias UIImage = NSImage
#endif

Scroll down to where the dog image is added to HStack in the ForEach. Add an #if check for macOS inside the if let photoData. Add the following above the Image(uiImage: uiImage), and follow it with #else:

#if os(macOS)
Image(nsImage: uiImage)
  .resizable()
  .scaledToFill()
  .frame(maxWidth: 80,
        maxHeight: 80)
  .clipShape(RoundedRectangle(
     cornerRadius: 5.0))
#else

This is a macOS call to load an Image with nsImage.

After the iOS image, add a closing #endif. The whole block will look like this when you’re done:

#if os(macOS)
Image(nsImage: uiImage)
  .resizable()
  .scaledToFill()
  .frame(maxWidth: 80,
        maxHeight: 80)
  .clipShape(RoundedRectangle(
        cornerRadius: 5.0))
#else
Image(uiImage: uiImage)
  // ... resizable, scaleToFill, frame, and clipShape stay
#endif

Xcode will dim the code not being used depending on the target you’ve chosen.

Next, choose EditDogView to fix the Image(uiImage: uiImage) the same way. Before the Image(uiImage: uiImage), add a Compiler Directive #if os(macOS) and add the NSImage version of the call, followed by #else. After the Image(uiImage: uiImage) and modifiers add #endif:

#if os(macOS)
Image(nsImage: uiImage)
  .resizable()
  .scaledToFit()
  .frame(maxWidth: .infinity,
         maxHeight: 300)
#else
Image(uiImage: uiImage)
  // ... resizable, scaleToFill, and frame stay
#endif

EditDogView also has a property to set the navigation bar title display mode, which isn’t available. You exclude that from macOS with #if os(macOS) wrapper. Add that to the .navigationBarTitleDisplayMode(.inline):

// EditDogView
#if !os(macOS)
.navigationBarTitleDisplayMode(.inline)
#endif

You’ll also need to make the same change in NewDogView.swift.

// NewDogView
#if !os(macOS)
.navigationBarTitleDisplayMode(.inline)
#endif

Switch back to EditDogView and fix the #Preview. NSImage doesn’t support pngData(). Simply set the sample dog’s image: to nil.

let dog = DogModel(
  name: "Mac",
  age: 11,
  weight: 90,
  color: "Yellow",
  image: nil // set the image to nil.
)

This is the last step in the app’s macOS compatibility. In the DogModel’s extension, duplicate the mock data with a version for macOS and a version for other platforms. Add the #if !os(macOS)... #else... #endif around the existing dog’s data. In the else, place a copy of the data with the images set to nil:

#if !os(macOS)
// ... existing dogs
#else
let macDog = DogModel(
  name: "Mac",
  age: 11,
  weight: 90,
  color: "Yellow",
  breed: labrador,
  image: nil,
  parks: [
    riverdale,
    withrow,
    kewBeach
  ]
)
let sorcha = DogModel(
  name: "Sorcha",
  age: 1,
  weight: 40,
  color: "Yellow",
  breed: golden,
  image: nil,
  parks: [
    greenwood,
    withrow
  ]
)
let violet = DogModel(
  name: "Violet",
  age: 4,
  weight: 85,
  color: "Gray",
  breed: bouvier,
  image: nil,
  parks: [
    riverdale,
    withrow,
    hideaway
  ]
)
let kirby = DogModel(
  name: "Kirby",
  age: 11,
  weight: 95,
  color: "Fox Red",
  breed: labrador,
  image: nil,
  parks: [
    allan,
    greenwood,
    kewBeach
  ]
)
let priscilla = DogModel(
  name: "Priscilla",
  age: 17,
  weight: 65,
  color: "White",
  breed: mixed,
  image: nil,
  parks: [])
#endif

Select the My Mac device, and build and run. You can add some dogs, and if you’ve used the same Apple ID on your iOS device, iCloud will sync your dogs.

Scaling Swift Data

As the app grows, you may want to consider optimizing the fetch with concurrency. Performing work on the main thread could become blocked if the app gets busy. Open AllNewDogs from the Project Navigator. You can find the file in the Starter folder if you’re building your own project.

Currently, the view displays all the mock dogs in the DogModel. Modify the file to test async/await for creating a large amount of data. Change the preview to use inMemory storage with the DogModel, not the preview. Replace the previews modelContainer:

// #Preview
.modelContainer(for: DogModel.self, inMemory: true)

Below the view’s closing curly brace, and a new function to create a dog record. The function is marked with async, so it doesn’t block the app.

func insertDog(name: String) async {
  modelContext.insert(DogModel(
    name: name,
    breed: BreedModel(
            name: "none")
    )
  )

  try? modelContext.save()
}

Add a toolbar with a + button to create 500 records. Place the toolbar after navigationTitle.

.toolbar {
  Button("", systemImage: "plus") {
    Task {
      for i in 1...500 {
        await
        insertDog(name: "Rover \(i)")
      }
    }
  }
}

Notice that await is placed before the insert.

The code in this part of the demo is going to attempt to create 500 dogs. Before you run in the Simulator, switch over to GoodDogApp in the Project Navigator. Change the configuration name to AllGoodDogs, so you don’t overwrite your current data.

let config = ModelConfiguration("AllGoodDogs", schema: schema)

Change to initial view from DogListView to AllNewDogs.

WindowGroup {
  AllNewDogs()
    .modelContainer(container)
}

Also go to the project file’s Signing & Capabilities and uncheck iCloud. You don’t want to sync these test dogs.

Build and run in the Simulator, tap the plus button. The app will attempt to insert dogs asynchronously, and the UI may respond to scrolling.

Using the new @ModelActor is more efficient and protects the data because it uses a serial thread. It uses a BackgroundActor() that you pass in a modelContainer.

Add a new function above the insertDog function:

@ModelActor
actor BackgroundActor {
  func insertDog(name: String) {
    modelContext.insert(
      DogModel(
        name: name,
        breed: BreedModel(
          name: "test"
        )
      )
    )
  try? modelContext.save()
  }
}

Inside the Button, add a modelContext container.

let container = modelContext.container

Update the Task closure with .detached property.

Task.detached { // ... }

Inside the Task.detached closure, add the BackgroundActor() before the for loop.

let backgroundActor = BackgroundActor(
  modelContainer: container)

Update the call to insertDog with the backgroundActor.

await backgroundActor.insertDog(
  name: "Rover \(i)")

The add toolbar button should look like this.

Button("", systemImage: "plus") {
  let container = modelContext.container
  Task.detached {
    let backgroundActor = BackgroundActor(
      modelContainer: container
    )
    for i in 1...500 {
      await backgroundActor.insertDog(name: "Rover \(i)")
    }
  }
}

Build and run the app on the Simulator. Tap the +. With the new @ModelActor, the insert will happen much faster and be safer.

That’s all for this demo. Before you leave, undo the changes in the GoodDogApp file.

let config = ModelConfiguration("GoodDogs", schema: schema)
WindowGroup {
  DogListView()
    .modelContainer(container)
}

You can leave iCloud unchecked. You won’t need it in the next lesson. Continue to the conclusion of this lesson.

See forum comments
Cinema mode Download course materials from Github
Previous: CloudKit Support & Extending SwiftData Apps Introduction Next: CloudKit Support & Extending SwiftData Apps Conclusion