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 for all your CoreData entities that were defined in the schema.
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)
}
}
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:
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.
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.
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:
- Modeling Data in SwiftData
- SwiftData Stack: Understanding Schema, Container & Context
- Introduction to Data Persistence in SwiftUI with SwiftData
- Data Handling in SwiftData: Create, Read, Update, Delete
- How to fetch and filter data in SwiftData with Predicates
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.