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

Heads up... You’re accessing parts of this content for free, with some sections shown as obfuscated text.

Heads up... You’re accessing parts of this content for free, with some sections shown as obfuscated text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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.

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
[x] Remote Notifications
iCloud.com.kodeco.GoodDog

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.

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

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.

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.

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

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.

#if os(macOS)
import Cocoa
typealias UIImage = NSImage
#endif
#if os(macOS)
Image(nsImage: uiImage)
  .resizable()
  .scaledToFill()
  .frame(maxWidth: 80,
        maxHeight: 80)
  .clipShape(RoundedRectangle(
     cornerRadius: 5.0))
#else
#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
#if os(macOS)
Image(nsImage: uiImage)
  .resizable()
  .scaledToFit()
  .frame(maxWidth: .infinity,
         maxHeight: 300)
#else
Image(uiImage: uiImage)
  // ... resizable, scaleToFill, and frame stay
#endif
// EditDogView
#if !os(macOS)
.navigationBarTitleDisplayMode(.inline)
#endif
// NewDogView
#if !os(macOS)
.navigationBarTitleDisplayMode(.inline)
#endif
let dog = DogModel(
  name: "Mac",
  age: 11,
  weight: 90,
  color: "Yellow",
  image: nil // set the image 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

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.

// #Preview
.modelContainer(for: DogModel.self, inMemory: true)
func insertDog(name: String) async {
  modelContext.insert(DogModel(
    name: name,
    breed: BreedModel(
            name: "none")
    )
  )

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

let config = ModelConfiguration("AllGoodDogs", schema: schema)
WindowGroup {
  AllNewDogs()
    .modelContainer(container)
}
@ModelActor
actor BackgroundActor {
  func insertDog(name: String) {
    modelContext.insert(
      DogModel(
        name: name,
        breed: BreedModel(
          name: "test"
        )
      )
    )
  try? modelContext.save()
  }
}
let container = modelContext.container
Task.detached { // ... }
let backgroundActor = BackgroundActor(
  modelContainer: container)
await backgroundActor.insertDog(
  name: "Rover \(i)")
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)")
    }
  }
}
let config = ModelConfiguration("GoodDogs", schema: schema)
WindowGroup {
  DogListView()
    .modelContainer(container)
}
See forum comments
Cinema mode Download course materials from Github
Previous: CloudKit Support & Extending SwiftData Apps Introduction Next: CloudKit Support & Extending SwiftData Apps Conclusion