It has happened! The long-awaited successor to CoreData
has been released—SwiftData
! Although SwiftData
retains some of CoreData
's characteristics (uses the same storage architecture), it does enable us to persist data using simple, declarative code. It seems like it may be a game changer for Swift developers, so I immediately had to check it out.
As I delved into basic use cases for SwiftData
, like To-Do apps, I noticed that the use of SwiftData
comes down to around 6 steps. In this article, I will guide you through the process of building a simple To-Do App using SwiftData
, shedding light on these basic steps. The article will not cover the app's UI implementation, as the emphasis is on SwiftData
.
Here are the six steps we'll cover:
-
import SwiftData
. - Create your model: Define a class for your data.
- Set your model to the
modelContainer
: This lets SwiftData know what data it will be working with. - Declare
@Environment(\.modelContext)
in your view: This establishes the context in which your data will exist and be manipulated. - Prefix
@Query
to an array variable: A wrapper to access your stored data. - Interact with your context for CRUD operations.
Let's get started by diving into each of these steps in more detail.
1. Import SwiftData
The first step is to import SwiftData
. Yes, I know it is obvious, but we all forget sometimes.
import SwiftData
2. Create your Model
In past, when using CoreData
, you would open a .xcdatamodel
file and implement an Entity by adding it with "Add Entity". In SwiftData
we simply model data with ordinary Swift types using @Model
. Here, I placed the @Model
wrapper above my data class ToDoItem
. This model will represent the individual tasks in our To-Do list. Each task will have a title, content, and the date it was added.
@Model
class ToDoItem {
let title: String
let content: String
let dateAdded: Date
init(title: String, content: String, dateAdded: Date = .init()) {
self.title = title
self.content = content
self.dateAdded = dateAdded
}
}
3. Set .modelContainer()
in App
In SwiftData
, the .modelContainer()
is used to establish the storage area or "container" for your data model within your application. Think of it as designating a specific space within your app to store all the data instances of the ToDoItem
model.
The container is set within the main app structure (in this case, SwiftDataToDoExampleApp
because this ensures the data is accessible throughout your entire application.
@main
struct SwiftDataToDoExampleApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: ToDoItem.self)
}
}
In this code snippet, .modelContainer(for: ToDoItem.self)
is added to WindowGroup
. This tells the application to create a storage area specifically for ToDoItem
instances. Now, wherever we are in the app, we can manipulate our to-do items.
4. Declare @Environment(\.modelContext)
in your view
Next, declare @Environment(.modelContext) within your View. @Environment(\.modelContext)
allows us to read from our container. It sounds a bit complex, but it's essentially a way to establish a connection between your View and the data stored in your modelContainer from step 3. In other words, It acts like a portal, that our View can use to interact with the modelContainer.
struct ContentView: View {
@Environment(\.modelContext) var context
// ...
}
5. Prefix @Query
to an array variable
In SwiftUI
view, you can fetch data using the wrapper @Query
. SwiftData
and SwiftUI
work in together, and when the underlying data changes, the view is automatically updated with the most recent data.
@Query var items: [ToDoItem]
Just like in CoreData
, you can add SortDescriptor
and Predicate
to a fetch. In the case below, the ToDoItem
's will be displayed in the reverse order of when they were added.
@Query(FetchDescriptor(sortBy: [SortDescriptor(\.dateAdded, order: .reverse)]),animation: .snappy) private var items: [ToDoItem]
6. Interact with your context for CRUD operations
Now that we have completed the set up of our model, container, context, and the way to communicate with the view, it is time to interact with our context. The first interaction with the context will be inserting data(ToDoItem
) into the context. After inserting a ToDoItem
into the context, don't forget to save it. This is a two-step process, and the changes made to the context are finalized by saving.
In the snippet below I have created two functions one to insert the item and one to then save it into the context.
func insert(_ item: ToDoItem) {
context.insert(item)
save()
}
func save() {
do {
try context.save()
} catch {
print(error.localizedDescription)
}
}
The same goes for deletion. Delete from the context and then save to the context to make sure it has been updated.
func delete(_ item: ToDoItem) {
context.delete(item)
save()
}
And with the above 6 steps, we have implemented SwiftData
into our To-do app. I will include all of the UI code below so that you are able to see how I put together the whole project. Feel free to copy and paste it into Xcode.
Finally, remember that SwiftData
is still in beta, and it can randomly crash, especially after saving. These issues will be updated as Xcode 15 comes out of beta, I imagine.
App Screenshot
The Code
ContentView.swift
import SwiftUI
import SwiftData
struct ContentView: View {
@Environment(\.modelContext) var context
@Query var items: [ToDoItem]
@State private var showingSheet: Bool = false
@State private var title: String = ""
@State private var content: String = ""
var body: some View {
NavigationStack {
List {
ForEach(items) { item in
VStack(alignment: .leading) {
HStack(alignment: .top) {
Text(item.title)
.bold()
Spacer()
Text(item.dateAdded.formatted(date: .numeric, time: .shortened))
.font(.caption)
.foregroundStyle(.gray)
}
Text(item.content)
.font(.subheadline)
}
.swipeActions(content: {
Button(action: {
self.delete(item)
}, label: {
Label("Delete", systemImage: "xmark.bin.fill")
})
.tint(Color.red)
})
}
}
.navigationTitle("To Do List")
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button {
showingSheet.toggle()
} label: {
Image(systemName: "plus")
}
}
}
.sheet(isPresented: $showingSheet, content: {
Form {
Section {
TextField("Add the title...", text: $title)
} header: {
Text("Title")
}
Section {
TextEditor(text: $content)
} header: {
Text("Content / Notes")
}
Section {
Button("Add Item") {
let item = ToDoItem(title: title, content: content)
insert(item)
showingSheet = false
title = ""
content = ""
}
.disabled(title.isEmpty || content.isEmpty)
}
}
})
}
}
func insert(_ item: ToDoItem) {
context.insert(item)
save()
}
func delete(_ item: ToDoItem) {
context.delete(item)
save()
}
func save() {
do {
try context.save()
} catch {
print(error.localizedDescription)
}
}
}
ToDoItem.swift
import SwiftUI
import SwiftData
@Model
class ToDoItem {
let title: String
let content: String
let dateAdded: Date
init(title: String, content: String, dateAdded: Date = .init()) {
self.title = title
self.content = content
self.dateAdded = dateAdded
}
}
SwiftDataToDoExampleApp.swift
import SwiftUI
@main
struct SwiftDataToDoExampleApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: ToDoItem.self)
}
}
Thanks for checking out the post.
All the best!
Dean Thompson
Follow me!
LinkedIn
Twitter
Instagram
References
Great example using SwiftData from KavSoft
Great article by Alexander Logan. Also goes into depth on using SwiftData with relationships: Alexander Logan's Blog
Top comments (0)