Francesco Leoni
Posted on June 14, 2023
Here you can find this articles and many more about iOS and macOS Development.
SwiftData is a framework for data modelling and management.
It's built on top of CoreData's persistence layer, but with an API completely redesigned and reimagined for Swift.
SwiftData provides also support for undo and redo automatically.
And if your app have shared container enabled, SwiftData automatically makes data directly accessible by widgets using the same APIs.
Now, let's see how it works.
Models
First we need to create the models that we want to persist.
To create a model you need to mark the class
with the Macro @Model
.
SwiftData includes all non-computed properties of a class as long as they use compatible types. So far, SwiftData supports types such as Bool
, Int
and String
, as well as structures
, enumerations
, and other value types that conform to the Codable
protocol.
So, let's create the Folder
and Note
models.
@Model
final class Folder {
@Attribute(.unique)
var id: String = UUID().uuidString
var name: String
var creationDate: Date
var notes: [Note] = []
init(name: String) {
self.name = name
self.creationDate = .now
}
}
@Model
final class Note: Decodable {
@Attribute(.unique)
var id: String = UUID().uuidString
var text: String
init(text: String) {
self.text = name
}
- Create a model class that contains all the properties you wish to persist
- Mark the classes with the Macro
@Model
- Optionally, add Macros (
@Attribute
,@Relationship
,@Transient
) to the property that need them
Creating models is as simple as that.
WARNING: As for now, if you declare an array of objects is likely you will get an error like
Ambiguous use of 'getValue(for:)'
. This is a bug in Xcode 15.0 beta that I think will be fixed on final version of Xcode. For now, just remove the@Model
from theNote
class and add@Transient()
tonotes
property.
Model attributes
Properties in the @Model
can have Macros attached to them that will defined their behaviour.
Currently there are 3 types of macros: @Attribute
, @Relationship
, @Transient
.
@Attribute
This Macro alters how SwiftData handles the persistence of a particular model property.
This is commonly used to mark a property as primary key, as we did for Folder.id
.
@Attribute(.unique)
var id: String
But there are many other options:
-
encrypt
: Stores the property’s value in an encrypted form. -
externalStorage
: Stores the property’s value as binary data adjacent to the model storage. -
preserveValueOnDeletion
: Preserves the property’s value in the persistent history when the context deletes the model. -
spotlight
: Indexes the property’s value so it can appear in Spotlight search results. -
transformable
: Transforms the property’s value between an in-memory form and a persisted form. -
transient
: Enables to the context to disregard the property when saving the model. -
unique
: Ensures the property’s value is unique across all models of the same type. ### @Relationship By default if you declare a property whose type is also a model, SwiftData manages the relationship between these models automatically. But if you want to customise its behaviour, you can use the@Relationship
Macro.
@Relationship(.cascade)
var notes: [Note] = []
This way if the Folder
is deleted, all of its notes are deleted too.
This Macro defines the behaviour that in CoreData was named Delete Rule.
NOTE: Both
@Attribute
and@Relationship
support the renaming of the argument in case you want to preserve the original name of the property.
@Transient
By default SwiftData persist all non-computed properties inside the model, but if you don't need to persist a specific property, you can add the @Transient
Macro to that property.
@Transient
var someTempProperty: String
The model container
Before using these models, we need to tell SwiftData which models to persist.
To do this there is the .modelContainer
modifier. To use it you simply pass all the models you want to persist.
@main
struct NoteBookApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: [Folder.self]) // <-
}
}
Here we pass only the Folder
class because SwiftData knows that it has to persist also Note
since there is a relationship between the two classes.
The .modelContainer
modifer allows you to specify some options.
-
inMemory
: Whether the container should store data only in memory. -
isAutosaveEnabled
: If enabled you don't need to save changes. -
isUndoEnabled
: Allows you to undo or redo changes to the context. -
onSetup
: A callback that will be invoked when the creation of the container has succeeded or failed.
NOTE: If you model contains a relationship to another model, you can omit the destination model.
Saving models
Now, we are ready to use SwiftData.
Let's see how to save models.
First we need to get the context, to do this we use the @Environment(\.modelContext)
wrapper.
@Environment(\.modelContext) private var context
Next, once we have the context, we create the object we want to save and we insert it into the context.
let folder = Folder(name: "Trips")
context.insert(folder)
If we have enabled the autosaving while creating the container we don't have to do anything else.
Otherwise we need to save the context.
do {
if context.hasChanges {
context.save()
}
} catch {
print(error)
}
Fetching models
To fetch existing models, SwiftData provides the @Query
wrapper.
This wrapper allows you to sort, filter and order the result of the query.
@Query(filter: #Predicate { $0.name != "" },
sort: \.name,
order: .forward)
var folders: [Folder]
Here we query by:
- Filter folders that have non-empty name
- Sort them by the
name
property - Order them ascending
Or, we can use a FetchDescriptor
.
extension FetchDescriptor {
static var byName: FetchDescriptor<Folder> {
var descriptor = FetchDescriptor<Folder>(
predicate: #Predicate { $0.id != "" },
sortBy: [SortDescriptor(\.name)]
)
descriptor.fetchLimit = 50
descriptor.includePendingChanges = true
return descriptor
}
}
@Query(.byName)
var items: [Folder]
This way we can reuse the FetchDescriptor
for as many queries as we need.
Updating models
To update a model we just need to change the value of the property we want and if autosave is enabled that's all.
folder.name = "New name"
Otherwise we need to save the context.
Deleting models
To delete a model is just as simple as to create one.
We get the instance of the object to delete and we pass it to the context.
context.delete(model)
Conclusion
SwiftData really simplifies the usage of CoreData, making it less prone to errors.
Of course this is still in Beta so there will be changes and improvements, but so far I really enjoyed playing with it.
Hope this will help developers speed up their workflow to persist data and reduce crashes.
Thank you for reading!
What are you thoughts about this? Tweet me @franceleonidev and share your opinion.
Posted on June 14, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.