Data Persistence with SwiftData

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

Lesson 05: SwiftData, Migrations & Working with Core Data

SwiftData Migrations 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

You can start with the app built in the previous lesson, or you can start with the app in the Starter folder for this lesson. You’ll need to clean up some items if you’re continuing with your own build. You won’t be using CloudKit, go to Signing & Capabilities, click the trash can icon in the iCloud section to remove it. Next, click the trash can icon in the Background Modes. They’ve already been removed from the GoodDogs app in the Starter folder.

let labrador = BreedModel(name: "Labrador Retriever")
let golden = BreedModel(name: "Golden Retriever")
let bouvier = BreedModel(name: "Bouvier")
let mixed = BreedModel(name: "Mixed")

let riverdale = ParkModel(name: "Riverdale Park")
let withrow = ParkModel(name: "Withrow Park")
let greenwood = ParkModel(name: "Greewood Park")
let hideaway = ParkModel(name: "Hideaway Park")
let kewBeach = ParkModel(name: "Kew Beach Off Leash Dog Park")
let allan = ParkModel(name: "Allan Gardens")

let macDog = DogModel(
  name: "Mac",
  age: 11,
  weight: 90,
  color: "Yellow",
  image: nil
)
let sorcha = DogModel(
  name: "Sorcha",
  age: 1,
  weight: 40,
  color: "Yellow",
  image: nil
)
let violet = DogModel(
  name: "Violet",
  age: 4,
  weight: 85,
  color: "Gray",
  image: nil
)
let kirby = DogModel(
  name: "Kirby",
  age: 11,
  weight: 95,
  color: "Fox Red",
  image: nil
)
let priscilla = DogModel(
  name: "Priscilla",
  age: 17,
  weight: 65,
  color: "White",
  image: nil
)

container.mainContext.insert(macDog)
macDog.breed = labrador
macDog.parks = [riverdale, withrow, kewBeach]
container.mainContext.insert(sorcha)
sorcha.breed = golden
sorcha.parks = [greenwood, withrow]
container.mainContext.insert(violet)
violet.breed = bouvier
violet.parks = [riverdale, withrow, hideaway]
container.mainContext.insert(kirby)
kirby.breed = labrador
kirby.parks = [allan, greenwood, kewBeach]
container.mainContext.insert(priscilla)
priscilla.breed = mixed

Setting Up Version 1.0.0

The first thing to do is to create the initial version of the VersionedSchema. Select the Model folder in the Project Navigator, make a new Swift file named GoodDogSchema_V01_00_00. At the top of the file, import SwiftData and add a MARK for version 1.0.0.

// MARK: - Version 1.0.0
enum GoodDogSchema_V01_00_00: VersionedSchema {
  static var versionIdentifier = Schema.Version(1, 0, 0)

  static var models: [any PersistentModel.Type] {
    [DogModel.self,
    BreedModel.self,
    ParkModel.self]
  }

  // ... you'll paste the DogModel here.
}

// paste the DogModel extension here
@Model
class DogModel {
  var name: String = ""
  var age: Int?
  var weight: Int?
  var color: String?
  var breed: BreedModel?
  @Attribute(.externalStorage) var image: Data?
  var parks: [ParkModel]?

  init(
    name: String,
    age: Int = 0,
    weight: Int = 0,
    color: String? = nil,
    breed: BreedModel? = nil,
    image: Data? = nil,
    parks: [ParkModel]? = nil
  ) {
    self.name = name
    self.age = age
    self.weight = weight
    self.color = color
    self.breed = breed
    self.image = image
    self.parks = parks
  }
}
extension DogModel {
  @MainActor
  static var preview: ModelContainer {
    do {
      let container = try ModelContainer(
          for: DogModel.self,
          configurations: ModelConfiguration(
            isStoredInMemoryOnly: true)
        )

      let labrador = BreedModel(name: "Labrador Retriever")
      let golden = BreedModel(name: "Golden Retriever")
      let bouvier = BreedModel(name: "Bouvier")
      let mixed = BreedModel(name: "Mixed")

      let riverdale = ParkModel(name: "Riverdale Park")
      let withrow = ParkModel(name: "Withrow Park")
      let greenwood = ParkModel(name: "Greenwood Park")
      let hideaway = ParkModel(name: "Hideaway Park")
      let kewBeach = ParkModel(name: "Kew Beach Off Leash Dog Park")
      let allan = ParkModel(name: "Allan Gardens")

  let macDog = DogModel(
    name: "Mac",
    age: 11,
    weight: 90,
    color: "Yellow",
    image: nil
  )
  let sorcha = DogModel(
    name: "Sorcha",
    age: 1,
    weight: 40,
    color: "Yellow",
    image: nil
  )
  let violet = DogModel(
    name: "Violet",
    age: 4,
    weight: 85,
    color: "Gray",
    image: nil
  )
  let kirby = DogModel(
    name: "Kirby",
    age: 11,
    weight: 95,
    color: "Fox Red",
    image: nil
  )
  let priscilla = DogModel(
    name: "Priscilla",
    age: 17,
    weight: 65,
    color: "White",
    image: nil
  )

      container.mainContext.insert(macDog)
      macDog.breed = labrador
      macDog.parks = [riverdale, withrow, kewBeach]
      container.mainContext.insert(sorcha)
      sorcha.breed = golden
      sorcha.parks = [greenwood, withrow]
      container.mainContext.insert(violet)
      violet.breed = bouvier
      violet.parks = [riverdale, withrow, hideaway]
      container.mainContext.insert(kirby)
      kirby.breed = labrador
      kirby.parks = [allan, greenwood, kewBeach]
      container.mainContext.insert(priscilla)
      priscilla.breed = mixed

      return container
    } catch {
      print("Fatal Error: Could not create preview modelContainer.")
      // Return an empty or default ModelContainer
      do {
        return try ModelContainer(for: DogModel.self, configurations: ModelConfiguration(isStoredInMemoryOnly: true))
      } catch {
        fatalError("Failed to create fallback ModelContainer.")
      }
    }
  }
}

// MARK: - Version 1.0.0 extension
extension GoodDogSchema_V01_00_00.DogModel {
  // ... preview code is here.
}

TypeAlias to the Rescue

Now you’ll fix some errors. Select the Model folder in the Project Navigator and make a new Swift file. Name the file GoodDogMigrationPlan. At the top of the file, import SwiftData. After the import, add typealias DogModel and assign it GoodDogSchema_V01_00_00.DogModel. Add another MARK. This is to solve some of the compiler errors.

// MARK: - MODEL TYPE ALIASES
typealias DogModel = GoodDogSchema_V01_00_00.DogModel
// ... DogModel

@Model
class BreedModel {
  var name: String = "Unknown Breed"
  var dogs: [DogModel]?

  init(name: String) {
    self.name = name
  }
}

@Model
class ParkModel {
  var name: String = ""
  var dogs: [DogModel]?

  init(name: String, dogs: [DogModel]? = nil) {
    self.name = name
    self.dogs = dogs
  }
}
typealias BreedModel = GoodDogSchema_V01_00_00.BreedModel
typealias ParkModel = GoodDogSchema_V01_00_00.ParkModel
let schema = Schema(GoodDogSchema_V01_00_00.DogModel.self)

Completing the Migration Plan

You’ll now update the app by adding a city to the ParkModel. This is going to be a lightweight migration. Some people, however, may choose to skip this update in production. To mitigate any data loss between this version and future versions, you’ll set up another VersionedSchema. Select the version 1.0.0 GoodDogSchema_V01_00_00 file in the Project Navigator. From the File menu, choose Duplicate. Name the new file GoodDogSchema_V01_01_00.swift.

// MARK: - Version 1.1.0
enum GoodDogSchema_V01_01_00: VersionedSchema {
  static var versionIdentifier = Schema.Version(1, 1, 0)

  // ...
}
// MARK: - Version 1.1.0 extension
extension GoodDogSchema_V01_01_00.DogModel {
  @MainActor
  // ... the preview data

}
enum GoodDogMigrationPlan: SchemaMigrationPlan {

}
static var schemas: [any VersionedSchema.Type]

static var stages: [MigrationStage]
static var schemas: [any VersionedSchema.Type] {
  [
    GoodDogSchema_V01_00_00.self,
    GoodDogSchema_V01_01_00.self
  ]
}
static let migration_V1_0_0_to_V1_1_0 = MigrationStage.lightweight(
  fromVersion: GoodDogSchema_V01_00_00.self,
  toVersion: GoodDogSchema_V01_01_00.self)
static var stages: [MigrationStage] {
  [migration_V1_0_0_to_V1_1_0]
}
enum GoodDogMigrationPlan: SchemaMigrationPlan {

  static var schemas: [any VersionedSchema.Type] {
    [
      GoodDogSchema_V01_00_00.self,
      GoodDogSchema_V01_01_00.self
    ]
  }

  static let migration_V1_0_0_to_V1_1_0 = MigrationStage.lightweight(
    fromVersion: GoodDogSchema_V01_00_00.self,
    toVersion: GoodDogSchema_V01_01_00.self)

  static var stages: [MigrationStage] {
    [migration_V1_0_0_to_V1_1_0]
  }
}

Adding City to Parks

Inside the GoodDogSchema_V01_01_00, update the ParkModel with an Optional String named city.

@Model
class ParkModel {
  // ...
  var city: String?

  init(name: String,
       dogs: [DogModel]? = nil,
       city: String?) {
    // ...
    self.city = city
  }
  // ...
}
let riverdale = ParkModel(
  name: "Riverdale Park",
  city: "Mississauga"
)
let withrow = ParkModel(
  name: "Withrow Park",
  city: "Toronto"
)
let greenwood = ParkModel(
  name: "Greenwood Park",
  city: "Burlington"
)
let hideaway = ParkModel(
  name: "Hideaway Park",
  city: "Hamilton"
)
let kewBeach = ParkModel(
  name: "Kew Beach Off Leash Dog Park",
  city: "Toronto"
)
let allan = ParkModel(
  name: "Allan Gardens",
  city: "Toronto"
)
// comment out the data in  GoodDogSchema_V01_00_00

//  let labrador = BreedModel(name: "Labrador Retriever")
//  let golden = BreedModel(name: "Golden Retriever")
//  let bouvier = BreedModel(name: "Bouvier")
//  let mixed = BreedModel(name: "Mixed")

// ...

//      kirby.breed = labrador
//      kirby.parks = [allan, greenwood, kewBeach]
//      container.mainContext.insert(priscilla)
//      priscilla.breed = mixed

typealias DogModel = GoodDogSchema_V01_01_00.DogModel
typealias BreedModel = GoodDogSchema_V01_01_00.BreedModel
typealias ParkModel = GoodDogSchema_V01_01_00.ParkModel

Updating the Park Views

Open the NewParkView in the Project Navigator. Add a state variable city with an empty string at the top of the NewParkView struct.

@State private var city = ""
LabeledContent {
  TextField("City", text: $city)
} label: {
  Text("Name")
    .foregroundStyle(.secondary)
}
let newPark = ParkModel(name: name, city: city)
VStack {
  Text(park.name)
    .font(.caption)
  Text(park.city ?? "n/a")
    .font(.caption2)
}
let riverdale = ParkModel(
  name: "Riverdale Park",
  city: "Toronto"
)
let withrow = ParkModel(
  name: "Withrow Park",
  city: "Toronto"
)
let greenwood = ParkModel(
  name: "Greenwood Park",
  city: "Toronto"
)

Using the Migration

Now that you’ve updated the model and views, it’s time to use the migration in the app. Switch to the GoodDogApp.swift and change schema from GoodDogSchema_V01_00_00.DogModel.self to GoodDogSchema_V01_01_00.DogModel.self, where the schema is defined in the container variable:

let schema = Schema([GoodDogSchema_V01_01_00.DogModel.self])
let container = try ModelContainer(
  for: schema,
  migrationPlan: GoodDogMigrationPlan.self,
  configurations: config
)

Custom Migration

Now that you’ve managed to add the city field, it’s time to reduce any redundant and duplicated entries. Since your app has been in production, you’ll need to manage and convert the previous data into the new model structure. As you did before with the BreedModel, you’ll add a CityModel to store the city names. This is going to be a major change and will require a custom migration. You’ll set up the data clean-up logic.

// MARK: - Version 2.0.0
enum GoodDogSchema_V02_00_00: VersionedSchema {
  static var versionIdentifier = Schema.Version(2, 0, 0)

  // ...
}
// MARK: - Version 2.0.0 extension
extension GoodDogSchema_V02_00_00.DogModel {
  @MainActor
  // ... the preview data

}
typealias DogModel = GoodDogSchema_V02_00_00.DogModel
typealias BreedModel = GoodDogSchema_V02_00_00.BreedModel
typealias ParkModel = GoodDogSchema_V02_00_00.ParkModel
[
  GoodDogSchema_V01_00_00.self,
  GoodDogSchema_V01_01_00.self,
  GoodDogSchema_V02_00_00.self
]
static let migrationV1_1_0toV2_0_0 = MigrationStage.custom (
  fromVersion: GoodDogSchema_V01_01_00.self,
  toVersion: GoodDogSchema_V02_00_00.self
) { context in
    // willMigrate: before migration
  } didMigrate: { context in
    // didMigrate: after migration
}
@Model
class CityModel {
  var name: String = ""
  var park: ParkModel?

  init(name: String) {
    self.name = name
  }
}
// ... ParkModel
var city: CityModel?

init(
  name: String,
  dogs: [DogModel]? = nil,
  city: CityModel?
) {

// ...
}
static var models: [any PersistentModel.Type] {
  [
    DogModel.self,
    BreedModel.self,
    ParkModel.self,
    CityModel.self
  ]
}
typealias CityModel = GoodDogSchema_V02_00_00.CityModel
let toronto = CityModel(name: "Toronto")
let hamilton = CityModel(name: "Hamilton")
let ottawa = CityModel(name: "Ottawa")
let mississauga = CityModel(name: "Mississauga")
let riverdale = ParkModel(
  name: "Riverdale Park",
  city: nil
)
let withrow = ParkModel(
  name: "Withrow Park",
  city: mississauga
)
let greenwood = ParkModel(
  name: "Greewood Park",
  city: hamilton
)
let hideaway = ParkModel(
  name: "Hideaway Park",
  city: toronto
)
let kewBeach = ParkModel(
  name: "Kew Beach Off Leash Dog Park",
  city: toronto
)
let allan = ParkModel(
  name: "Allan Gardens",
  city: nil
)
Text(park.city?.name ?? "n/a")
let riverdale = ParkModel(
  name: "Riverdale Park",
  city: CityModel(name:"Toronto")
)
let withrow = ParkModel(
  name: "Withrow Park",
  city: CityModel(name:"Toronto")
)
let greenwood = ParkModel(
  name: "Greenwood Park",
  city: CityModel(name:"Toronto")
)
let newPark = ParkModel(
  name: name,
  city: CityModel(
    name: city
  )
)
//      let labrador = BreedModel(name: "Labrador Retriever")
//      let golden = BreedModel(name: "Golden Retriever")
//      let bouvier = BreedModel(name: "Bouvier")
//      let mixed = BreedModel(name: "Mixed")
//
// ...
//
//      container.mainContext.insert(violet)
//      violet.breed = bouvier
//      violet.parks = [riverdale, withrow, hideaway]
//      container.mainContext.insert(kirby)
//      kirby.breed = labrador
//      kirby.parks = [allan, greenwood, kewBeach]
//      container.mainContext.insert(priscilla)
//      priscilla.breed = mixed

// Park to city mapping for migrationV1_1_0toV2_0_0
  static var parkToCityDictionary: [String: String] = [:]
// willMigrate: before migration
guard let parks = try? context.fetch(
  FetchDescriptor<GoodDogSchema_V01_01_00.ParkModel>()
) else {
  return
}
// save the mapping
parkToCityDictionary = parks.reduce(into: [:], { dictionary, ParkModel in
  dictionary[ParkModel.name] = ParkModel.city
})
// after migration
let uniqueCities = Set(parkToCityDictionary.values)
// add cities to ParkModel
for city in uniqueCities {
  context.insert(GoodDogSchema_V02_00_00.CityModel(name: city))
}
try? context.save()

guard let parks = try? context.fetch(
  FetchDescriptor<GoodDogSchema_V02_00_00.ParkModel>()
) else {
  return
}
guard let cities = try? context.fetch(
  FetchDescriptor<GoodDogSchema_V02_00_00.CityModel>()
) else {
  return
}
// match park to city
for parkToCity in parkToCityDictionary {
  guard let parkModel = parks.first(where: {
    $0.name == parkToCity.key
  }) else {
    return
  }
  guard let cityModel = cities.first(where: {
    $0.name == parkToCity.value
  }) else {
    return
  }
  parkModel.city = cityModel
  try? context.save()
}
static var stages: [MigrationStage] {
  [
    migration_V1_0_0_to_V1_1_0,
    migration_V1_1_0_to_V2_0_0
  ]
}
let schema = Schema([GoodDogSchema_V02_00_00.DogModel.self])
See forum comments
Cinema mode Download course materials from Github
Previous: Migrations & Working with Core Data Instruction Next: From Core Data to SwiftData Demo