In the constantly evolving world of SwiftUI app development, having a user-friendly and efficient way to handle data persistence is crucial. In this blog post, I introduce you to SwiftData – a new, Swift-native persistence framework. Designed to work seamlessly with SwiftUI, SwiftData can make your data management tasks much easier and faster than ever before.
We’ll explore how SwiftData, with its use of native Swift types and modern features, can simplify your interactions with persistence compared to older frameworks like CoreData. You’ll learn how to define models, use SwiftData in a SwiftUI project, perform CRUD operations, and navigate some of the limitations and bugs in the current version. SwiftData is using the new Observation feature for a simpler and more effective data flow.
Requirement: Xcode 15
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
What is SwiftData?
SwiftData is a new Swift-native persistence framework. It is built with the Swift language. SwiftData is made to work well with SwiftUI.
Therefore, it makes it much easier and faster to persist the user’s data in your SwiftUI apps.
What is the difference between SwiftData and CoreData?
SwiftData is a new Swift-native persistence framework. CoreData is around for quite a while and was built with Objective-C. Therefore you have to deal with older types in CoreData. SwiftData is built with Swift and allows you to use Swift native types.
Additionally, SwiftData uses a lot of newer features like Swift concurrency and Swift macros.
SwiftData uses a lot of the same technologies as CoreData for managing the underlying database. But the way you write your code with SwiftData is much easier and Swift-like than CoreData.
If you have an existing project in CoreData, you can migrate it to SwiftData relatively easily and keep the same database. Have a look here for a full walk-through.
I see SwiftData as the new and easier version of CoreData. Apple took the technologies that work well from CoreData and added all the newer tools to work best with SwiftUI.
Creating Models for SwiftData
In SwiftData models are defined as classes. To add a model definition, you can decorate your class with the @Model macro:
import Foundation
import SwiftData
@Model final public class Folder {
var creationDate: Date
var name: String
@Attribute(.unique) var uuid: UUID
@Relationship(.cascade, inverse: \Snippet.folder) var snippets: [Snippet]?
init(name: String = "",
snippets: [Snippet] = []) {
self.creationDate = Date()
self.uuid = UUID()
self.name = name
self.snippets = snippets
}
}
SwiftData will automatically create a Schema from your model file. Your model will also have ´PersistentModel´ conformance:
protocol PersistentModel : AnyObject, Observable, Hashable, Identifiable
In the above example, I also used other macros. @Attribute is a macro that allows you to finetune the attribute storage. For example, you can make a unique property like so:
@Attribute(.unique) var uuid: UUID
The @Relationship macro allows me to customize the delete rules.
Read more: Modeling Data in SwiftData
Using SwiftData together with SwiftUI
SwiftData works well in a SwiftUI project. I will show you how quickly you can get data persistence in your app.
Setting up a new project with Xcode 15
You can now set up the modelContainer for your SwiftData project in the main app file. The modelContainer is a modifier that ensures all windows in the group are configured to access the same persistent container.
import SwiftUI
import SwiftData
@main
struct SnippetBoxApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.modelContainer(for: [Snippet.self, Folder.self, Tag.self])
}
}
}
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. You can access the model context in any of your views from the environment:
import SwiftUI
import SwiftData
struct ContentView: View {
@Environment(\.modelContext) private var context
var body: some View {
...
}
}
Saving changing in SwiftData
SwiftData automatically saves changes to the underlying database. This happens when SwiftUI views update, which allows you to keep your user’s data consistent. You don’t need to manually call save.
Working with SwiftData: Create, Read, Update, and Delete your Data
All-access to your data in SwiftData happens with the model context. For example to fetch all folders in the database, fetch the data from the context:
var folders = try context.fetch(FetchDescriptor<Folder>())
In SwiftUI you can fetch all folders with the `@Query` property wrapper:
import SwiftUI
import SwiftData
struct FolderListView: View {
@Environment(\.modelContext) private var context
@Query(sort: \Folder.creationDate, order: .forward)
private var folders: [Folder]
@Binding var selectedFolder: Folder?
var body: some View {
List(selection: $selectedFolder) {
ForEach(folders) { folder in
NavigationLink(value: folder) {
FolderRow(folder: folder, selectedFolder: selectedFolder)
}
}
.onDelete(perform: deleteItems)
}
.navigationTitle("Folders")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
}
}
private func addFolder() {
...
}
private func deleteItems(offsets: IndexSet) {
...
}
}
You can filter your query with the new predicate macro. For example, the Snippet type has a `isFavorite` boolean property. We can filter the query and only show the favorite snippets:
@Query(filter: #Predicate<Snippet> { $0.isFavorite },
sort: [SortDescriptor(\.creationDate)] )
var favoriteSnippets: [Snippet]
See more complex and advanced predicates: How to fetch and filter data in SwiftData with Predicates
You can then create new objects and insert them into the model context to persist it like so:
let folder = Folder(name: "new folder")
context.insert(folder)
For example, I added a `New Folder` button in the above folder list view.
Additionally, you use the context to delete objects in SwiftData:
@Environment(\.modelContext) private var context
private func delete(folder: Folder) {
context.delete(folder)
}
How to update SwiftData objects
Updating model objets is very simple. You set the property of the object to the new value. For example, if you want to change the ´isFavorite´ property of a snippet object, you can set it to true like so:
import SwiftUI
struct SnippetDetailView: View {
var snippet: Snippet
var body: some View {
Button {
snippet.isFavorite = true
} label: {
Label("Mark as Favorite", systemImage: "heart")
}
...
}
}
SwiftData uses the new Observation API from SwiftUI. When you add the ´@Model´ macro to your classes, they will conform to the new Observable protocol. This API is much better at updating SwiftUI views when model properties changes. Thus, you should benefit from a better performance when using SwiftData.
I you want to work with a view like `Toggle `in SwiftUI that requires a binding, you can use the `@Bindable` property wrapper like so:
import SwiftUI
struct SnippetDetailView: View {
@Bindable var snippet: Snippet
var body: some View {
Toggle(snippet.isFavorite ? "favorite" : "not",
isOn: $snippet.isFavorite)
...
}
}
SwiftData and Xcode Preview
Using SwiftData with Xcode previews is a struggle. Some of Apple’s demo projects gave up on previews completely and removed them from all files. However, here is a working solution. It is more complex than I would prefer.
First define a preview container that is in memory and repopulate it with your data:
let previewContainer: ModelContainer = {
do {
let container = try ModelContainer(for: Snippet.self,
configurations: ModelConfiguration(isStoredInMemoryOnly: true))
Task { @MainActor in
let context = container.mainContext
let snip = Snippet.example2()
let folder = Folder(name: "folder with favorite snippet")
context.insert(folder)
folder.snippets.append(Snippet(isFavorite: true,
title: "favorite snippet"))
// add test data here
}
return container
} catch {
fatalError("Failed to create container: \(error.localizedDescription)")
}
}()
Advanced SwiftData
Can I sync SwiftData across devices?
You can use iCloud to sync your SwiftData across devices. This works for a private database but not for shared or public database, for which you would relay again on CoreData. You can learn about SwiftData container configurations in this post SwiftData Stack: Understanding Schema, Container & Context
Can I use SwiftData together with CoreData in the same project?
SwiftData and CoreData are compatible. You can configure your storage so that both SwiftData and CoreData references the same SQLight database file.
How to migrate from CoreData to SwiftData and keep the existing database file?
You can migrate an existing CoreData file to SwiftData. Xcode has same handy tools that easily generate SwiftData model classes from CoreData schemas. For a full walk-through have a look at this post: How to convert a CoreData project to SwiftData
Missing Features
SwiftData is in its first version and is thus missing quite a few features. Here is a list of what you might miss out on:
- sectioned queries are not supported
- limited support of predicates with relationships
- predicate support for case insensitive text filtering
- predicates with enum properties
- to-many relationships do not keep their order
- iCloud sync works only with a private database
If you absolutely need some of these missing features, I would recommend working with CoreData instead. CoreData is much more mature and is integrated well with SwiftUI.
Limitations and Problems for SwiftData with Xcode 15
I encountered quite a few bugs and problems with SwiftData. Here are some of the most prominent ones:
- app crashes when you use redo enabled container and custom types or enums as properties
- to-many relationships with iCloud sync need to be optional. You have to work with optional arrays
- to-many relationships are defined as arrays to other model classes. This makes the impression that they keep their sort order, but this is not the case, and the array is returned in seemingly random order.
- app crashes after data migrations
Conclusion
SwiftData introduces an intuitive, Swift-native approach to handling data persistence in SwiftUI apps. It utilizes Swift’s native types, along with features like concurrency and macros, simplifying your coding experience compared to CoreData.
With SwiftData, models are easily created as classes and decorated with various macros, allowing for detailed attribute and relationship management. Implementing SwiftData in SwiftUI is straightforward, facilitating seamless data persistence in your apps.
However, SwiftData is a new framework and comes with certain limitations and bugs that need to be ironed out. Yet, its core concept of providing a Swift-native and user-friendly persistence framework holds potential.
If you’re developing a SwiftUI app or considering migrating from CoreData, SwiftData might be worth exploring. It can streamline your coding experience, though keep in mind the specific needs of your project before making a decision. Happy coding!
Further Reading:
- Modeling Data in SwiftData
- SwiftData Stack: Understanding Schema, Container & Context
- Data Handling in SwiftData: Create, Read, Update, Delete
- How to fetch and filter data in SwiftData with Predicates
- How to convert a CoreData project to SwiftData
- SwiftData is using the new Observation for a simpler and more effective data flow.