Open the GoodDog
app from the Starter
folder. It’s already been created as a multi-platform SwiftUI app but without any data storage. Build and Run the app in the Simulator. You’ll see that it already has an app icon, start screen, and displays a single record in the DogListView
, SwiftUI file.
The app also has a DogModel
Swift file, which sets up a dog name, age, weight, color, and breed. You’ll modify these properties as you go through the lessons, but for now, they’re either String or Int types, and some are Optional types.
Select the DogModel
from the Project navigator, and in the Editor pane, add import SwiftData
at the top of the file.
import SwiftData
Inside the DogModel class, add @Model
to the class declaration line. You can either place the @Model macro on the start of the line or just above. Both are valid.
@Model
class DogModel { // ... }
or
@Model class DogModel { // ... }
Build the app with the Command-B
or by choosing Build
from the Project
menu.
Pro Tip: when the compiler notices a change in the code, it will try to update the Canvas Preview. You can use
Command-B
to start the update as well.
Behind the scenes, SwiftData is setting the DogModel up as a SwiftData model. SwiftData sets up the Persistent Store, modelContainer, and modelContext with the properties defined in the model.
Note: Your model class will always require an init method for SwiftData, even if you provide default values. For the lessons here, you’ll name the models with the
Model
suffix for clarity. In common practice, the dog model would simply be namedDog
and referred to in the app as aDog
or instance of the Dog model.
Select the GoodDogApp.swift
file in the Project navigator. Import SwiftData at the top of the file. In the Window Group
, just below the DogListView()
, add the .modelContainer
modifier. Here, you’ll list your models in an array. Add the following.
WindowGroup {
DogListView()
.modelContainer(for: DogModel.self)
}
Since you only have one model at the moment, you don’t need to add the square brackets. You’ve added the modelContainer to the DogListView
. If you have more views in your WindowGroup, you can move the modelContainer down outside the WindowGroup to apply to the whole group of views.
Note: You’ll see later, when you add more models and create relationships with
DogModel
, that you won’t need to list all the models. That’s another nice thing about SwiftData. In some cases, you may need to list more models, but for most of these lessons, you won’t.
So far, you’ve created the model and modelContainer. Next, you would fetch
the data from the data store and display it. Select the DogListView.swift
from the Project navigator.
Import SwiftData at the top of the file. Inside the DogListView
struct, add the @Query
as a private var dogs
, of type DogModel
. Put the model in square brackets since you’re fetching multiple dogs here.
@Query private var dogs: [DogModel]
Imaginary Dogs with Mock Data
Currently, your data store has no data. To create some mock data for the Canvas preview, head back to the DogModel. Add an extension on the DogModel. At the top add @ModelActor
, which is a special concurrency macro to have the preview
actions run on the main thread.
Here, you’ll add a preview instance of the ModelContainer for DogModel.self
and a ModelConfiguration setting to store in-memory. In other words, you’re creating data objects for the preview that are only persisted while the Canvas view is active. You can find the mock data code in Resources.txt
in the Starter folder. Here you’re using the mainContext, a ModelContext type, which is a property of the ModelContainer. Add this to the bottom of the DogModel
file, below the last curly brace.
extension DogModel {
@MainActor
static var preview: ModelContainer {
let container = try! ModelContainer(
for: DogModel.self,
configurations: ModelConfiguration(isStoredInMemoryOnly: true))
container.mainContext.insert(DogModel(
name: "Mac",
age: 11,
weight: 90))
container.mainContext.insert(DogModel(
name: "Sorcha",
age: 1,
weight: 40))
container.mainContext.insert(DogModel(
name: "Violet",
age: 4,
weight: 85))
container.mainContext.insert(DogModel(
name: "Kirby",
age: 10,
weight: 95))
return container
}
}
Go back to DogListView
and fetch that mock data. Embed the HStack inside a List, step through the fetched dogs, and return a dog object on each row. Replace the hard-coded string with the fetched dog’s name. You’ll add a dog.image
in a later lesson.
List(dogs) { dog in
HStack {
Image(systemName: "dog")
.imageScale(.large)
.foregroundStyle(.tint)
Text(dog.name)
}
}
Wait a minute! The Canvas preview is now blank. You need to load the mock data for the preview. Add the modelContainer modifier to the DogListView()
in #Preview
, with an instance of .preview
instead of .self
.
#Preview {
DogListView()
.modelContainer(DogModel.preview)
}
Check it out. You’ve just fetched data with SwiftData and displayed in a List. Keep in mind that the modelContainer will set up the store, add the DogModel
and make a context that you’ll use later.
This is one of many ways to display mock data, and you’ll use this often. There are cases where you’ll use DogModel.self
and make the mock data directly in the preview, as you may have done in other apps.
New Dogs, New Tricks
If you run the app on the Simulator, you’ll still have an empty view. To remedy that, you’ll make use of a view to make new dogs and another view to edit the dog records. You’ll start by adding a NavigationStack
to the view. Embed the List in a NavigationStack and set the .navigationTitle
to “Good Dogs” as a modifier to the List.
NavigationStack {
List(dogs) { dog in
// ...
}
.navigationTitle("Good Dogs)
}
While you’re there, add a modifier to the HStack to use the .title
font.
HStack {
// ...
}
.font(.title)
Below the .navigationTitle, add a .toolbar
with ToolbarItem
containing an “Add New Dog” Button
with systemImage plus
. Leave the action closure empty for now.
.toolbar {
ToolbarItem(placement:
.primaryAction) {
Button("Add New Dog",
systemImage: "plus") {
// action closure
}
}
}
From the Project navigator, select the NewDogView.swift
. It’s been set up with a few State properties, and you’ll need to update it for SwiftData. Since this view isn’t going to be fetching data, you don’t need to import SwiftData. Instead, you’ll make use of the Environment
property to connect to the modelContainer you added to DogListView
in the App file. You’ll use the modelContext that SwiftData created for this. The modelContainer in the app file sets up the permanent storage, adds the model to the storage, and loads the model into the modelContext.
At the top of the NewDogView
struct, add an instance of the environment modelContext.
@Environment(\.modelContext)
private var modelContext
Now you can fill out the action in the Create button to save your first dog. You do this by adding a newDog
model with data values and insert
the model. SwiftData will save the newDog into the persisted store. By default, SwiftData is configured to autosave
changes in the modelContext. Later, you’ll see times where you explicitly call save
, as you may have done in Core Data apps.
Add these two lines to the Create Button’s action:
let newDog = DogModel(name: name)
modelContext.insert(newDog)
Switch back to DogListView
and add a State property to show the NewDogView
with a default false value. When the Add button is tapped, you’ll toggle this value by setting it to true
.
@State private var showingNewDogScreen = false
Add a .sheet(isPresented: )
below the .toolBar
. When the $showingNewDogScreen
is true, you’ll show the NewDogView as a medium size sheet.
.sheet(isPresented: $showingNewDogScreen) {
NewDogView()
.presentationDetents([.medium, .large])
}
Note: Here, the
.large
expands the sheet to full height when activated. You can leave this out if you like.
Now, when the compiler checks the build, you’ll see an error asking you to pass in name
because it’s required. The preview will also fail. If you look through the Canvas preview diagnosis, you’ll see:
Compiling failed: missing arguments for parameters 'name' in call
While refactoring parts of a SwiftData app, you will encounter various errors. You will learn a few ways to fix the previews along the way.
Since this a new dog, those values aren’t known yet. Enter an empty string as the name value. The Create
button on the NewDogView is disabled when the name field is empty, so this will work.
.sheet(isPresented: $showingNewDogScreen) {
NewDogView(name: "")
.presentationDetents(
[.medium, .large]
)
}
In the Add New Dog
button, set the property to true
.
Button("Add New Dog", systemImage: "plus") {
showingNewDogScreen = true // add this
}
Now that the preview is fixed, you can try to create a new dog right in Xcode’s Canvas preview. From the DogListView, tap the +
in the Canvas. The new dog sheet slides up. Enter Fido and press Create. Swipe down on the sheet to dismiss it. There’s the new dog created in memory and the modelContext has updated the List to show all the dogs. Nice!
It would also be nice if the sheet could dismiss when saving, too. Go back to NewDogView
and add the @Environment’s dismiss
function. The environment has many of these actions available. At the top of the struct, add the following:
@Environment(\.dismiss) private var dismiss
Then, under the modelContext, insert the call to dismiss. The button should now look like this.
Button("Create") {
let newDog = DogModel(name: name)
modelContext.insert(newDog)
dismiss()
}
Also, add dismiss()
to the Cancel
button action in case your user changes their mind.
Build and Run in the Simulator, and make a dog or two. Stop the Simulator in Xcode, and run the app again.
Check it out! The dog you created is now persisted in the store, fetched, and displayed in the List, as if by magic.
Visiting the Vet - Debugging Data
Putting the magic aside, debugging SwiftData and Core Data can seem like a magical mystery. However, there are several apps that let you take a peek at the data stores. For instance, you can use DB Browser for SQLite. You may wonder where the data is stored. You can use the Mac’s System Services
to find it.
Select the GoodDogApp.swift
from the Project navigator. Add an init method to show the path to the store in the debug console. Add the following at the bottom of the app struct:
init() {
print(URL.applicationSupportDirectory.path(percentEncoded: false))
}
Run the app in the Simulator. From the console, select the printed path up to the Library
. The space character in the Application Support
breaks the trick, so don’t include it. Right-click on the selected path, and from the contextual menu, choose Services
and Open
. The Mac will prompt you to confirm, so choose Run Service
.
The folder inside the Simulator’s app opens up. Open the Application Support
folder, and you should see three files:
default.store
default.store-shm
default.store-wam
They’re the same file types you get with a Core Data app. The name might be different, but you’ll see how to make your own name later.
Pro Tip: While in development, you might need to alter your data set. If you like the state of a set of records in the Simulator, you can copy and store the three files somewhere on your Mac. If you want to put them back, stop your app and put these files back in. Run the app in the Simulator, and your records should be back. This will only work until you make a big change to the data. If so, the app might crash in the Simulator. If that happens, delete the app from the Simulator and build a new version.
Select the default.store
, right-click and choose Open With > Other
. In the Applications folder that opens, choose DB Browser for SQLite
or your Core Data tool of choice.
In DB Browser for SQLite, choose the Browse Data
tab. From the Table
selector, choose ZDOGMODEL
. You should see the dogs you created in the app here. Z_PK
is the primary key, and you should see ZNAME
, listing the names of the records you made.
Neat huh? This will be super helpful when debugging your SwiftData and Core Data apps.
Deleting Bad Dogs
Along with creating new records, SwiftData can also delete them. It’s actually very straightforward.
@Query
by itself doesn’t have a delete
function. You’ll need to add another modelContext from the Environment at the top of the DogListView struct.
@Environment(\.modelContext) private var modelContext
You’ll need to refactor the List to contain a ForEach, since List doesn’t have an .onDelete
method. Change the List to a ForEach
and embed that in a plain List. After that at the bottom curly brace of the Foreach add an .onDelete(perform: )
. You’ll call a func called dogToDelete
, which you’ll add after this.
List {
ForEach(dogs) { dog in
// ...
}
.onDelete(perform: dogToDelete)
}
Pro Tip: If you double click the opening curly brace, Xcode will select the whole code block. That makes it easy to find the closing brace.
Add a func dogToDelete
inside the DogListView struct, to delete the dog at the List’s row index. Remember that the PersistentModel
protocol is Identifiable
.
func dogToDelete(indexSet: IndexSet) {
for index in indexSet {
modelContext.delete(dogs[index])
}
}
Since you now have mock data, you can test out the swipe to delete with the Canvas view. Don’t worry, the mock data will return the next time you build this view. You can also run the app in the Simulator and try out the deletion.
Note: You can also build and run on a device. Just change the
Bundle ID
to your own team’s bundle ID inSigning & Capabilities
.
Dog Training 101 - Editing Dogs
In the Project Navigator, select the EditDogView
. Notice that it’s already been set up with state properties similar to the model. It also has a PhotoUI PhotosPicker
already set up. There’s also a computed variable called changed
, so if any of the properties is changed, an Update
button will appear in the toolbar. Next, you’ll set this view up for SwiftData.
At the top of the file import SwiftData
:
import SwiftData
After the Environment dismiss
, add a bindable DogModel
with the following.
@Bindable var dog: DogModel
The preview will complain, but you’ll fix that soon.
Now, to load the data into the fields, you’ll use .onAppear
. After the navigation modifiers, add the onAppear with each property in DogModel
, with nil-coalescing operator
for the Optional values. Also, add a pragma mark to make this easier to find later as this view may grow.While your there add didAppear = true
to make the Showing the Update button smoother.
// MARK: onAppear
.onAppear {
name = dog.name
age = dog.age ?? 0
weight = dog.weight ?? 0
color = dog.color ?? ""
breed = dog.breed ?? ""
didAppear = true
}
To fix the preview, you’ll need to provide local mock data. Start by adding a modelContainer with DogModel
inside the preview. Then, make a dog with values. Lastly, return the EditDogView
with the container you made in the preview. Your preview will look like this:
#Preview {
let container = try! ModelContainer(for: DogModel.self)
let dog = DogModel(
name: "Mac",
age: 11,
weight: 90,
color: "Yellow",
breed: "Labrador Retriever")
return EditDogView(dog: dog)
.modelContainer(container)
}
Now that EditDogView
is ready, head back to DogListView
and add a NavigationLink(value:label:)
to the items in the Foreach. EditDogView
goes in the first closure, and the existing HStack goes in the label:
closure. In EditDogView
call, you’ll pass in the selected dog:
NavigationLink {
EditDogView(dog: dog)
} label: {
HStack {
Image(systemName: "dog")
.imageScale(.large)
.foregroundStyle(.tint)
Text(dog.name)
}
.font(.title)
}
Notice that a disclosure indicator is added to each row. Go ahead and try it out in the Canvas preview. The dog you tap should load into the EditDogView
. The Update
button still doesn’t appear when you change some values. To fix that, you need to change the changed
variable to watch for changes to the dog model object
. Update the changed
variable.
var changed: Bool {
name != dog.name
|| age != dog.age
|| weight != dog.weight
|| color != dog.color
|| breed != dog.breed
}
The last step is to assign the changed values and save when the Update
Button is tapped. Remember, autosave is enabled, so the data changes will be saved from the context.
In the Update button, assign the field values to the dog object before the dismiss():
Button("Update") {
dog.name = name
dog.age = age
dog.weight = weight
dog.color = color
dog.breed = breed
dismiss()
}
Doggy Picture Day
You’ve accomplished quite a lot so far, but you may be wondering about the Photo picker. To handle images, you’ll add a Data
property to the model. Since images can be large, you’ll also use the @Attribute
for external storage. SwiftData uses Data for binary types.
In the DogModel
, add an image var of type Data and add the attribute with the externalStorage setting.
@Attribute(.externalStorage) var image: Data?
Add the image to the initializer with a nil value.
, image: Data? = nil // NB add comma
}
// ...
self.image = image
}
Behind the scenes, SwiftData will perform a migration on the model changes to add an image property.
You’ll also need to fix the mock data with a nil value for the images. Add , image: nil
to each row. You’ll see how to mock images in a future lesson.
container.mainContext.insert(
DogModel(
name: "Mac",
age: 11,
weight: 90,
image: nil))
container.mainContext.insert(
DogModel(
name: "Sorcha",
age: 1,
weight: 40,
image: nil))
container.mainContext.insert(
DogModel(
name: "Violet",
age: 4,
weight: 85,
image: nil))
container.mainContext.insert(
DogModel(
name: "Kirby",
age: 10,
weight: 95,
image: nil))
In the EditDogView
, there’s already an image property as a State variable. Add the image in the changed
variable, in the onAppear
, and in the Update
button.
// var changed Bool
|| image != dog.image
// .onAppear {
image = dog.image
// Button("Update") {
dog.image = image // before dismiss()
Finally, add a .task
to use the photo picker to convert the image to Data asynchronously and assign it to the dog object’s image property. Put this code near where the .onAppear
and .toolbar
are located.
.task(id: selectedPhoto) {
// the photo picker has a protocol to convert to Data type
if let data = try? await selectedPhoto?.loadTransferable(type: Data.self) {
image = data
}
}
Phew! That’s a lot of work you’ve done. You’ve only scratched the surface of using SwiftData for CRUD operations. You learned how to fetch real and mock data with SwiftData, how to create records, delete records, and how to update records. As a bonus, you learned to add images to your data objects.
Congratulations on making a working SwiftData app. Now, continue to the next part for a summary.