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

From Core Data to SwiftData Demo

Episode complete

Play next episode

Next
Transcript

Moving From Core Data to SwiftData

In the Starter folder, you’ll find another app called CoreData_SwiftUI. This a simple SwiftUI app that uses Core Data as the persistence framework. You’ll convert this app to use **SwiftData** with the knowledge you’ve gained in this course.

Note: If you want to work with another Core Data app, make sure that its minimum iOS version is iOS 17, the first version that supports SwiftData.

Open the CoreData_SwiftUI from the File menu with the Open command or double-click the CoreData_SwiftUI.xcodeproj in the Finder. The app consists of a list of people in a SwiftUI List, similar to the GoodDog app. There’s an Add button that opens a form as a sheet to add a new person.

Click the CoreData_SwiftUI.xcdatamodeld in the Project Navigator. This will load the Core Data model editor in the center pane. Notice on the left, there’s an Entity named UserInfo. This is the model and is equivalent to a model you’ll create for the SwiftData version. There are three String type fields to store in the objects. These are firstName, lastName, and gender.

Note: The xcdatamodeld is often called the momd.

Tap the Show Navigators in the top-right corner of Xcode’s window. Select the UserInfo entity in the Entities. In the right navigator’s pane, select the Data Model Inspector in the Inspectors sidebar on the right. Notice that under Class, the option for CodeGen is set to Class Definition. With that default option, Core Data will generate the model class files when the app is run.

Click the Persistence.swift file in the Project Navigator. This file is used to configure the PersistenceController’s for the SwiftUI previews and the app. Notice the line that says, container = NSPersistentContainer(name: "CoreData_SwiftUI"). This is similar to the modelContainer you’ll set up soon. At the bottom of the file, is a function to make mock data for the Canvas previews. The previews are writing to "/dev/null" which is the equivalent to isStoredInMemoryOnly set to true.

Control-click the CoreData_SwiftUI gray folder in the Project Navigator and choose New Folder. Name the folder Models. You can also use the File menu by choosing New and then choosing Group from the hierarchal menu. Select the Model folder, and add a new Swift file. Name the file, UserInfoModel. For now, the name is different from the entity in the Core Data momd file.

At the top of the UserInfoModel file, import SwiftData. Create a class called UserInfoModel. Add the three variables for the model:

import Foundation
import SwiftData

class UserInfoModel {
  var firstName: String?
  var lastName: String?
  var gender: String?

}

At the end of the project, you’ll rename the model to match the original Core Data model.

Note: You would get an error saying Invalid redeclaration of 'UserInfo' if you named the model UserInfo, as the name collides with Core Data’s model entity. That would be ok because you’ll soon remove the CoreData_SwiftUI.xcdatamodeld file. Apple recommends renaming the entities on the Core Data side by prefixing the names with CD_ to avoid collisions when SwiftData and Core Data co-exist.

Below the variables, start to enter init..., and Xcode’s code completion will offer to write the init method. Press Return to accept it. Select the init code block and press Command-M to expand the method for easier reading. The init should appear like the following:

init(
  firstName: String? = nil,
  lastName: String? = nil,
  gender: String? = nil
) {
  self.firstName = firstName
  self.lastName = lastName
  self.gender = gender
}

At the beginning of the class, add SwiftData’s @Model macro. Press Command-B to set up the SwiftData model. Your whole model will look like the following:

import Foundation
import SwiftData

@Model
class UserInfoModel {
  var firstName: String?
  var lastName: String?
  var gender: String?

  init(
    firstName: String? = nil,
    lastName: String? = nil,
    gender: String? = nil
  ) {
    self.firstName = firstName
    self.lastName = lastName
    self.gender = gender
  }
}

Also, add the mock data extension to the UserInfoModel.

extension UserInfoModel {
  @MainActor
  static var preview: ModelContainer {
    do {
      let container = try ModelContainer(for: UserInfoModel.self, configurations: ModelConfiguration(isStoredInMemoryOnly: true))

      let user1 = UserInfoModel(
        firstName: "Taylor",
        lastName: "SwiftData",
        gender: "Female"
      )
      let user2 = UserInfoModel(
        firstName: "Joe",
        lastName: "Smith",
        gender: "Male"
      )
      let user3 = UserInfoModel(
        firstName: "Jane",
        lastName: "Doe",
        gender: "Female"
      )

      container.mainContext.insert(user1)
      container.mainContext.insert(user2)
      container.mainContext.insert(user3)

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

Select the CoreData_SwiftUIApp.swift file in the Project Navigator. At the top of the file, import SwiftData. Remove the line that creates the persistenceController, and remove the environment managedObjectContext from the UserListView():

@main
struct CoreData_SwiftUIApp: App {
 // 1. delete the persistenceController
 let persistenceController = PersistenceController.shared

 var body: some Scene {
  WindowGroup {
    UserListView()
      // 2. delete the environment
      .environment(
        \.managedObjectContext,
          persistenceController.container.viewContext
      )
  }
 }
}

Add the SwiftData style modelContainer to the UserListView().

UserListView()
  .modelContainer(for: [UserInfoModel.self])

Press Command-B to set up the SwiftData modelContainer and prepare the modelContext.

Select the UserListView.swift file in the Project Navigator, to prepare it for SwiftData. Replace import CoreData with import SwiftData at the top of the file.

Inside the UserListView struct, the environment managedObjectContext variable for CoreData already exists. Update it to use the newer \.modelContext keypath for SwiftData.

@Environment(\.modelContext) private var modelContext

The existing @FetchRequest has an empty array of SortDescriptors and references the Core Data UserInfo model type. Replace the Core Data style @FetchRequest with a SwiftData @Query for the UserInfoModel.self, sorting by firstName.

@Query(
  sort: \UserInfoModel.firstName,
  order: .forward
) var users: [UserInfoModel]

Update the preview by replacing Core Data .environment(\.managedObjectContext, //... ), with the SwiftData, using the preview in the UserInfoModel.

.modelContainer(UserInfoModel.preview)

Open the UserInfoView.swift in the Project Navigator. Update the environment managedObjectContextvariable to use the newer \.modelContext keypath for SwiftData.

// Update the environment managedObjectContext to this
@Environment(\.modelContext) private var modelContext

Update the addItem function by replacing the newItem constant as you’ve done before. Remove the do-catch block as well. Add the call to dismiss() if you removed it.

/* Replace the current addItem code
let newItem = UserInfo(context: modelContext)
newItem.firstName = firstName
newItem.lastName = lastName
newItem.gender = gender

// remove the do-catch block
*/

let newUser = UserInfoModel(
  firstName: firstName,
  lastName: lastName,
  gender: gender
)
modelContext.insert(newUser)
dismiss()

Go back to UserListView and update the sheet presentation of UserInfoView. Remove passing in the Core Data style environment modelContext.

.sheet(isPresented: $showingAddUser) {
  UserInfoView()
}

Remove the Persistence.swift and CoreData_SwiftUI.xcdatamodeld since they’re no longer needed. Now you’re clear to rename the user info model to UserInfo to match the original data model. You’ll do that ina moment.

Recall that the PersistenceController referred to the container as CoreData_SwiftUI. This means that the data store files would be named the same:

CoreData_SwiftUI.sqlite
CoreData_SwiftUI.sqlite-shm
CoreData_SwiftUI.sqlite-wal

Also, recall that SwiftData’s naming uses the default name unless you specify a name. Update the CoreData_SwiftUIApp with a custom ModelConfiguration.

Add a variable container of type ModelContainer:

var container: ModelContainer

Add an init() to configure the data store’s SQLite location using the Application Support directory. Add the store’s path to a modelConfiguration and configure the container in a do-try-catch.

init() {
  do {
    let storeURL = URL.applicationSupportDirectory.appending(
      path: "CoreData_SwiftUI.sqlite"
    )
    let config = ModelConfiguration(url: storeURL)
    container = try ModelContainer(
      for: UserInfoModel.self,
      configurations: config
    )
  } catch {
    fatalError("Failed to configure SwiftData container.")
  }
}

Add the debug statement to print the store’s location at the bottom of the init:

print(URL.applicationSupportDirectory.path(percentEncoded: false))

One last step to go! Select the property called UserInfoModel. Control-click it and choose Refactor > Rename. This will open Xcode’s rename function. This should select all references to the UserInfoModel. Change the name to UserInfo, which is the same as the Core Data Entity.

Now that the model matches the original Entity and the SQLite file locations are the same, you can build and run.

That ends the demo on Migrations and Core Data. Continue on the the conclusion.

See forum comments
Cinema mode Download course materials from Github
Previous: SwiftData Migrations Demo Next: Migrations & Working with Core Data Conclusion