How to convert a CoreData project to SwiftData

SwiftData is a new Swift-native persistence framework. CoreData and SwiftData share the same underlying database file. If you have an existing project in CoreData, you can migrate it to SwiftData relatively easily. In this blog post, I am going to go through the process step-by-step.

I will start by examining how you can generate SwiftData Model Classes, utilizing the Managed Object Model Editor assistant. Then, I’ll take you through a complete SwiftData adoption for an existing Core Data application. For those who aren’t ready for a full transition, I’ll delve into the coexistence between Core Data and SwiftData.

By the end of this post, you’ll have a clear understanding of how to generate model classes, handle a full transition, or coexist with Core Data. So, if you’re ready to venture into the world of SwiftData and unlock its potential, continue reading as I guide you through this transformative process.

I’ll use a demo project called ‘SnippetBox’ – a tool that helps programmers save and organize frequently used code snippets – to bring the concepts to life.

⬇️ Download the project from GitHub https://github.com/gahntpo/SnippetBox-SwiftData

Requirement: Xcode 15

Generating Model Classes for SwiftData

Xcode can help you write the model classes from a CoreData schema. Select the xcdatamodeld file in the navigator area. Open the ´Editor´ menu and choose ´Create SwiftData Code´. Follow the instructions in the popup window.

Xcode generates model files from core data to swiftdata
Xcode can automatically generate SwiftData model files from a CoreData schema definition.

Xcode generates model files for all your CoreData entities that were defined in the schema.

xcode Swiftdata model generation results

Xcode shows a lot of error messages because you will have now multiple class definitions with the same names: one set defined in the .xcdatamodeld file and your SwiftData files. Please delete now the .xcdatamodeld file if you do not want to use CoreData any longer. 

Keeping both CoreData and SwiftData in the same project

You need to have different names for your SwiftData and CoreData classes. You can rename CoreData classes in the schema editor. In the following example, the CoreData class is renamed to CDTrip and e.g. used in SwiftUI like so:

struct ContentView: View {
   @ObservedObject var trip: CDTrip
   var body: some View {
     Text(trip.name)
  }
}
xcode CoreData renaming class names
Change the class name in the model editor (red box). The Entity name should stay the same because this is the reference name in the underlying database (green box).

Writing initializers for your SwiftData models

SwiftData models are class and thus don’t get a default initializer. You need to provide your own initialisers. Luckily Xcode can help you with that and automatically generate inits for you. Simply start typing init and use Xcode auto-completion. Here is an example file:

import SwiftData

@Model final public class Snippet {
    var code_: String?
    var creationDate_: Date?
    @Attribute(.externalStorage) var image: Data?
    var isFavorite: Bool? = false
    var language_: String?
    var notes_: String?
    var title_: String?
    var uuid_: UUID?
    @Relationship(inverse: \Folder.snippets_) var folder: Folder?
    @Relationship(inverse: \Tag.snippets_) var tags_: [Tag]?

    init(
        code_: String? = nil,
        image: Data? = nil,
        isFavorite: Bool? = nil,
        language_: String? = nil,
        notes_: String? = nil,
        title_: String? = nil,
        folder: Folder? = nil,
        tags_: [Tag]? = nil
    ) {
        self.creationDate_ = Date() // set current date when object is created
        self.uuid_ = UUID() // set new uuid when object is created
        self.image = image
        self.code_ = code_
        self.isFavorite = isFavorite
        self.language_ = language_
        self.notes_ = notes_
        self.title_ = title_

        self.folder = folder
        self.tags_ = tags_
    }
}

Note that I am setting the creation date and uuid properties in the initializer. 

Read more: Modeling Data in SwiftData

Handling Optionals and Renaming Attributes

In CoreData you have to deal with optional handling. One way of dealing with this is to write computed properties. I use the underbar notation for my attributes in the xcdatamodeld file. For example, I defined a title_ attribute and added this computed property in an extension to my model class:

import CoreData
extension Snippet {
   var title: String {
        get { self.title_ ?? "" }
        set { self.title_ = newValue  }
    }
}

In SwiftData you do not have to deal with this cumbersome optional handling. You can simply declare:

@Model final public class Snippet {
    var title: String
    
    init(title: String, ...) {
      self.title = title
    }
}

Since I want to change my schema, I need to tell Xcode how to map the old attributes to the new ones. There is the possibility to rename attributes. Simply specify the old attribute names with ´@Attribute(originalName:)´ like so:

@Model final public class Snippet {

    @Attribute(originalName: "title_")
    var title: String
    
    init(title: String) {
       self.title = title
    }
}

Important Note: Schema changes with CoreData are quite flexible. However, if you use iCloud sync together with CoreData, you are much more limited and cannot do this kind of attribute renaming.

Setting up Relationships in SwiftData

The generated code gave me errors for the relationships. In the following example, I have a one-to-many relationship between folder and snippet. The delete rule for folder to snippet relationship is cascading. That means when I delete a folder all snippets in the folder are also deleted:

xcode SwiftData error with relationship

You should only set the ´@Relationship´attribute from one side. Xcode added this for both the snippet and folder in my data. I thus generated a circular reference. Simply removing the relationship from one side solved the problem.

@Model final public class Snippet {

     ...

    var folder: Folder?
    var tags_: [Tag]
    ...
}

Setting up a many-to-many relationship in SwiftData

You can define many-to-many relationships in SwiftData simply by defining properties with class types. For example, I am setting up a many-to-many relationship between Tag and Notes. A note can have many tags and a tag can be attached to many notes:

@Model final public class Tag {

     ...

    var notes: [Note]
    ...
}

Setting up the SwiftData stack and working with the context

Once you’ve created your SwiftData model classes, the next step is to set up the SwiftData stack. The SwiftData stack replaces the Core Data stack in your application, leveraging Swift-native language features. You can also delete ´PersistentController´ file.

You can now set up the modelContainer for your SwiftData stack. The modelContainer is a modifier that ensures all windows in the group are configured to access the same persistent container. 

xcode SwiftData error with relationship

I have to pass all model types that I want to use in my app. Since Snippet has references to my other types ´Folder´ and ´Tag´, the system automatically recognises them when I declare the container for ´Snippet´.

This not only sets up your container but also creates and sets a default ModelContext in the environment. The ModelContext is used to track changes to instances of your app’s types and can be accessed from within any scene or view using the environment property.

CoreData uses the managedObjectContext  from the environment:

@Environment(\.managedObjectContext) var context

SwiftData uses the modelContext:

@Environment(\.modelContext) private var context

Both CoreData and SwiftData contexts serve the same purpose.

Free SwiftUI Layout Book

Get Hands-on with SwiftUI Layout.

Master SwiftUI layouts with real-world examples and step-by-step guide. Building Complex and Responsive Interfaces.

Saving changes in SwiftData

Unlike CoreData, you don’t have to explicitly call save on the context. In SwiftData save happens automatically when main view updates happen. You can remove all code that handles save actions in CoreData.

If you don’t like this behavior, you can change the container configuration like so:

.modelContainer(for: Snippet.self, isAutosaveEnabled: false)

How to work with the Xcode preview and SwiftData

Similar to CoreData, SwiftData also requires a context to work with the preview. Here is an example where I set the default container:

import SwiftUI
import SwiftData

struct FolderListView: View {

    @Query(sort: \.creationDate, order: .forward)
    var folders: [Folder]
    
    var body: some View {
        List {
                ForEach(folders) { folder in
                    NavigationLink(value: folder) {
                        FolderRow(folder: folder, selectedFolder: selectedFolder)
                    }
                }
            }
        }
    }
}

#Preview {
    FolderListView(selectedFolder: .constant(nil))
        .modelContainer(for: Snippet.self)
}

Fetching Data in SwiftData with @Query

Fetching data also gets a facelift with SwiftData. You can use a Query to fetch a list of objects from the SwiftData container instead of using a fetch request like in Core Data

This is the fetch you would use in CoreData:

@FetchRequest(sortDescriptors: [SortDescriptor(\Folder.creationDate_)]) private var folders: FetchedResults<Folder>

which you can now relate with SwiftData query:

import SwiftUI
import SwiftData

struct FolderListView: View {

    @Query(sort: \Folder.creationDate, order: .forward) var folders: [Folder]

    var body: some View {
        List {
            ForEach(folders) { folder in
                 FolderRow(folder: folder)
            }
        }
    }
}

See example for fetching and filtering: How to fetch and filter data in SwiftData with Predicates

CoreData observation and modification in subviews with @ObservedObject:

struct FolderRow: View {
   @ObservedObject var folder: Folder
   var body: some View {
    ...
   }
}

becomes in SwiftData:

struct FolderRow: View {
   @Binding var folder: Folder
   var body: some View {
    ...
   }
}

Creating new SwiftData Objects

Creating a new object in SwiftData is simpler than in Core Data. For instance, you can create a new Trip with just one line of code, and insert it into the model context to persist it. To save this trip and future changes, SwiftData uses an implicit save feature that triggers on UI lifecycle events and on a timer after the context changes.

The following is an example to generate a new folder object and add it to the context:

@Environment(\.modelContext) private var context

private func addFolder() {
     let folder = Folder(name_: "new folder")
     context.insert(folder)
}

Deleting SwiftData Objects

Similar to CoreData you use the context to delete objects in CoreData:

@Environment(\.modelContext) private var context

private func delete(folder: Folder) {
    context.delete(folder)
}

Also, all objects have a context property that gives you the context they belong to. Here is how you could use it to streamline you delete function:

private func delete(folder: Folder) {
   if let context = folder.modelContext {
     context.delete(folder)
   }
}

Conclusion

To sum it all up, the shift from Core Data to SwiftData provides a host of benefits, such as improved code readability, the integration of Swift’s native language features, and an automatic save feature. The transition process involves generating SwiftData model classes, setting up the SwiftData stack, testing pre-existing Core Data model designs for compatibility, and removing Core Data components. While a complete transition may not be feasible for all applications, SwiftData offers an attractive, flexible approach for apps looking to leverage its features. Whether it’s a complete migration or a partial one, transitioning to SwiftData opens the door to a more streamlined, efficient data persistence experience.

Further Reading:

FAQ

What is SwiftData and why should I consider migrating from Core Data?

SwiftData is a Swift-native persistence framework designed to provide a simpler, more efficient way of managing your application’s data. By leveraging Swift’s native language capabilities, SwiftData can offer improved code readability and an automatic save feature, which aren’t as straightforward in Core Data.

How can I generate SwiftData model classes from my Core Data model?

You can generate SwiftData model classes from your Core Data model using the Managed Object Model Editor assistant. Select your model file, navigate to the menu bar, select Editor, and click on ‘Create SwiftData Code’ to generate files for your pre-existing entities.

How do I ensure that my pre-existing Core Data models are compatible with SwiftData?

For each entity defined in Core Data, there needs to be a corresponding model type in SwiftData, with exact matches for entity name and properties. You should thoroughly test these new models to verify their functionality and ensure compatibility.

How do object creation and data fetching in SwiftData differ from Core Data?

Object creation in SwiftData is more straightforward than in Core Data, requiring just a single line of code. You can then insert the new object into the model context for persistence. Fetching data in SwiftData uses the Query function, which replaces Core Data’s fetch request.

What Core Data components and features can I remove when migrating to SwiftData?

In transitioning to SwiftData, you can eliminate explicit save calls on the context, as SwiftData relies on implicit saves. Additionally, you can remove the Core Data managed object model file and the Persistence file, as these are replaced by SwiftData model classes and the modelContainer respectively.

What if a complete transition from Core Data to SwiftData isn’t feasible for my application?

If a complete transition isn’t feasible, SwiftData offers the flexibility of a partial conversion or coexistence. Coexistence is when there are two completely separate persistent stacks, one Core Data stack and one SwiftData stack, talking to the same persistent store.

Can Core Data and SwiftData coexist within the same application?

Yes, Core Data and SwiftData can coexist within the same application. This involves having two completely separate persistent stacks, one for Core Data and one for SwiftData, that both communicate with the same persistent store.

What factors should I consider before making the transition from Core Data to SwiftData?

Transitioning from Core Data to SwiftData is a decision that requires careful consideration of several factors. The first thing to note is that SwiftData is only available for iOS 17 and macOS 14 and later. If your app still supports older versions, a full transition might not be practical.

Another factor to consider is that although SwiftData simplifies many aspects of data management and improves code legibility, it is currently more limited in terms of features compared to Core Data. For instance, SwiftData does not support sectioned fetch requests and dynamic updating queries that Core Data offers.

Also, if you have a complex existing Core Data model design, the transition might be challenging. You would need to ensure that your current data model designs, which include entities, their properties, and relationships, are supported in SwiftData. It involves creating corresponding model types in SwiftData for each entity defined in Core Data, ensuring an exact match for entity name and properties.

Lastly, the testing phase of the transition could be extensive, requiring thorough verification that all features are working as expected in SwiftData.

In conclusion, while SwiftData offers several benefits, it’s important to consider these factors before initiating the transition. Depending on your specific needs and constraints, it might be more beneficial to stick with Core Data, go for a full transition to SwiftData, or adopt a hybrid approach where Core Data and SwiftData coexist.

Leave a Comment

Subscribe to My Newsletter

Want the latest iOS development trends and insights delivered to your inbox? Subscribe to our newsletter now!

Newsletter Form