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.
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.
DogModel
from the Project navigator, and in the Editor pane, add import SwiftData
at the top of the file.
import SwiftData
@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 { // ... }
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.
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)
}
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.
fetch
the data from the data store and display it. Select the DogListView.swift
from the Project navigator.
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]
@ModelActor
, which is a special concurrency macro to have the preview
actions run on the main thread.
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
}
}
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)
}
}
DogListView()
in #Preview
, with an instance of .preview
instead of .self
.
#Preview {
DogListView()
.modelContainer(DogModel.preview)
}
DogModel
and make a context that you’ll use later.
DogModel.self
and make the mock data directly in the preview, as you may have done in other apps.
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)
}
.title
font.
HStack {
// ...
}
.font(.title)
.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
}
}
}
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.
NewDogView
struct, add an instance of the environment modelContext.
@Environment(\.modelContext)
private var modelContext
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)
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
.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.
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.
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]
)
}
Add New Dog
button, set the property to true
.
Button("Add New Dog", systemImage: "plus") {
showingNewDogScreen = true // add this
}
+
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!
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()
}
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.
DB Browser for SQLite. You may wonder where the data is stored. You can use the Mac’s System Services
to find it.
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))
}
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
.
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.
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.
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.
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
.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.
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
.
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.
import SwiftData
:
import SwiftData
Environment dismiss
, add a bindable DogModel
with the following.
@Bindable var dog: DogModel
The preview will complain, but you’ll fix that soon.
.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
}
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)
}
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)
}
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
}
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()
}
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.
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.
, 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))
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()
.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.