During WWDC 2023, SwiftData was introduced as a powerful framework that simplifies data persistence and management with its declarative approach. Previously, you’ve probably used CoreData, a popular persistence framework. However, integrating Core Data into your app can sometimes be a complex endeavor, especially for defining the Schema. I see SwiftData as the next evolution of CoreData. While both frameworks serve the purpose of data management, SwiftData offers a streamlined and intuitive approach, especially when it comes to defining the schema.
In this blog post, I will show you how to define your schema with SwiftData.We will explore how SwiftData simplifies the process of data modeling and why it can be a better choice over Core Data, particularly when it comes to defining the schema directly in your code.
If you have an existing CoreData project, you can take advantage of Xcode tools that help generate the model files. Read more about the process in this post.
⬇️ download the project from GitHub https://github.com/gahntpo/SnippetBox-SwiftData
Requirement: Xcode 15
Defining a Data Model in Swift
I am using a SnippetBox project as a demo. SnippetBox is a handy tool designed to help programmers save and organize pieces of code that they use frequently. Think of it as a special box where you can store all your precious code snippets. With SnippetBox, you can easily add, edit, and categorize your snippets by language or tags. You can even group them into folders for better organization. No more wasting time searching for that piece of code you wrote a while ago. With SnippetBox, your code snippets are always just a few clicks away. Let’s dive in and see how it works!
I added different property types for Snippet like Date, UUID, and Bool. Some more advanced use cases are images and enums. Each Snippet can belong to one folder and a folder can have many snippets, which is represented by a one-to-many relationship. On the other hand, each snippet can have many tags, and a tag can be added to many snippets, which is a many-to-many relationship. In the following graph, you can see the schema.
Class vs Struct in Swift Data Modeling
SwiftUI works well with structs in order to properly update the views. For iOS 17 and macOS 14, SwiftUI did get a new Observation data flow, which allows using of classes as models. SwiftData uses these mechanisms. In SwiftData models are defined as classes, which is the same as in CoreData.
However, this will limit the use of SwiftData objects in view models like:
class ViewModel: ObservableObject {
@Published var snippet: Snippet
}
If you would use this view model in your SwiftUI views and changed a property in the object snippet, the objectWillChange in the view model will not be triggered and your views will not be updated. SwiftData objects are meant to be used directly in SwiftUI views as state properties.
Creating A SwiftData Schema and Understanding SwiftData’s Schema Macros
Macros make it easy to quickly add data persistence to your app and fine tune the schema definition. I will show you the most important macros that you can use with SwiftData.
If you have a model definition in code, you can decorate your class with the @Model macro:
import Foundation
import SwiftData
@Model final class Snippet {
var uuid: UUID
var creationDate: Date
var code: String
var image: Data?
var isFavorite: BooL
var notes: String
var title: String
var language: String?
var folder: Folder? = nil
var tags: [Tag]? = nil
}
SwiftData will automatically create a Schema from your model file. Your model will also have PersistentModel
conformance:
protocol PersistentModel : AnyObject, Observable, Hashable, Identifiable
You can see the new Observable protocol that will give you life updates when used in SwiftUI views. The model will also get an object identifier and becomes Identifiable.
One might think that the default behavior that SwiftData assigns to your models would be sufficient, and in many cases, it is. But, there may be times when you would want to adjust it to suit your specific needs better. This is where SwiftData’s schema macros really shine, offering you the flexibility to customize how your app interacts with persistent data. You will learn in the following sections about the ´@Attribute´ macro that allows you to e.g. add uniqueness constraints to properties and rename properties. You can also fine-tune how relationships work with the ´@Relationship´ macro, which helps you to set delete rules.
Writing initializers for your SwiftData models
SwiftData models are class and thus don’t get a default initializer. You need to provide your own initialisers. Luckily Xcode can help you with that and automatically generate inits for you. Simply start typing init and use Xcode auto-completion. Here is the Snippet model with a generated initializer:
import Foundation
import SwiftData
@Model final class Snippet {
let uuid: UUID
let creationDate: Date
var code: String
var image: Data?
var isFavorite: BooL
var notes: String
var title: String
var language: String?
var folder: Folder? = nil
var tags: [Tag]? = nil
init(
code: String = "",
image: Data? = nil,
isFavorite: Bool = false,
language_: String? = nil,
notes: String = "",
title: String = "",
folder: Folder? = nil,
tags_: [Tag] = []
) {
self.creationDate = Date()
self.uuid = UUID()
self.image = image
self.code = code
self.isFavorite = isFavorite
self.language_ = language_
self.notes = notes
self.title = title
self.folder = folder
self.tags = tags
}
}
Note that I am setting the ´creationDate´ and ´uuid´ properties in the initializer. You could add the default values directly to the properties like so:
@Model final class Snippet {
let uuid: UUID = UUID()
let creationDate: Date = Date()
...
}
However, this is not working correctly for Xcode 15. Apple suggests using the init as a fix.
What property types can I use with SwiftData
You can use basic types for your SwiftData properties including String, Date, and number types like Int, Float, and Double.
Optional and Non-Optional Types in SwiftData
SwiftData allows you to take advantage of Swift data types. You can define properties as optional and non-optional. If you use non-optional values, make sure to set default values. In the Snippet example, I set the String properties as empty strings in the initialiser:
@Model final class Snippet {
...
var code: String
var notes: String
var title: String
init(
code: String = "",
image: Data? = nil,
isFavorite: Bool = false,
language_: String? = nil,
notes: String = "",
title: String = "",
folder: Folder? = nil,
tags_: [Tag] = []
) {
...
self.code = code
self.notes = notes
self.title = title
}
}
I also want to allow the user to store an image for each code snippet. This is optional. My image data property is thus defined with an optional:
@Model final class Snippet {
...
var image: Data?
init(
code: String = "",
image: Data? = nil,
isFavorite: Bool = false,
language_: String? = nil,
notes: String = "",
title: String = "",
folder: Folder? = nil,
tags_: [Tag] = []
) {
...
self.image = image
}
}
Working With Custom Types for SwiftData properties
You can store custom types in SwiftData as long as you conform them to the Codable protocol. For example, I want to store a color type for my Tag model:
struct ColorData: Codable {
var red: Double = 1
var green: Double = 1
var blue: Double = 1
var opacity: Double = 1
var color: Color {
Color(red: red, green: green, blue: blue, opacity: opacity)
}
init(red: Double, green: Double, blue: Double, opacity: Double) {
self.red = red
self.green = green
self.blue = blue
self.opacity = opacity
}
init(color: Color) {
let components = color.cgColor?.components ?? []
if components.count > 0 {
self.red = Double(components[0])
}
if components.count > 1 {
self.green = Double(components[1])
}
if components.count > 2 {
self.blue = Double(components[2])
}
if components.count > 3 {
self.opacity = Double(components[3])
}
}
}
@Model final public class Tag {
...
var colorData: ColorData = ColorData(red: 1, green: 1, blue: 1, opacity: 1)
}
Note: Xcode was crashing on me when I used the redo feature together with custom types. Make sure to setup your model container like:
@main
struct SnippetBoxApp: App {
var body: some Scene {
WindowGroup {
ContentView()
//.modelContainer(for: Snippet.self, isUndoEnabled: true) // crashes when using custom type codable like enum
.modelContainer(for: Snippet.self)
}
}
}
Can you use Enums with SwiftData?
You can directly use enum types with your SwiftData properties, as long as they conform to Codable. For example, I want to store the programming language for each code snippet. If the programming language is defined as an enum:
enum CodingLanguage: String, Codable, CaseIterable, Identifiable {
case swift
case objectivec
case pyton
case css
case typescript
var id: Self { return self }
}
You can directly use this in the Snippet model like so:
@Model final class Snippet {
...
var codingLanguage: CodeEditor.Language = .swift
}
Note: Xcode was crashing on me when I used the redo feature together with custom types and enums. I also was not able to filter for enum properties with @Query and predicates.
Adjusting the Model Definition with the @Attribute macro
Once, you have defined your properties, you can further fine-tune your schema definition with Swift macros. The first macro that I am showing you is the @Attribute
macro.
@attached(member) public macro Attribute(_ options: PropertyOptions..., originalName: String? = nil, hashModifier: String? = nil)
Renaming a property in SwiftData with lightweight migration
You can rename a property with the @Attribute(originalName:)
macro. For example I can rename my folder name property like so:
@Model final class Folder {
@Attribute(originalName: "name") var newName: String
...
}
SwiftData takes care of the schema migration and you can access the database value now from the ´newName´ property.
@Attribute macro with property options
The following property options are available:
- unique: ensures the property’s value is unique across all models of the same type
- transient: enables the context to disregard the property when saving the owning model
- transformable: transforms the property’s value between an in-memory form and a persisted form
- externalStorage: stores the property’s value as binary data adjacent to the model storage
- encrypt: stores the property’s value in an encrypted form
- preserveValueOnDeletion: preserves the property’s value in the persistent history when the context deletes the owning model
- spotlight: indexes the property’s value so it can appear in Spotlight search results
I will show you examples of these in the following sections.
Adding Uniqueness Constraints to a Property in SwiftData
You can make a property in SwiftData unique like so:
@Model final class Folder {
@Attribute(.unique) public var id: String
...
}
If you try to insert an object with the same value id, the existing object will be updated with the value properties of the object you wanted to insert. This behavior is called an insert.
For example, use a unique id for data in an app, where you fetch from the server and store locally in SwiftData. If you add the same object id again, the existing object is updated. This can help you keep your app’s data up-to-date and consistent.
Unique constraints work with primate value types like Numeric, String, and UUID.
Non-persistent Data with the @Transient Macro
If you do not want to store property in your database and you only want to cash it, you can use the @Transient
macro:
@Attribute(.transient)
var title: String = ""
or the @Attribute(.transient)
macro like so:
@Transient
var title: String = ""
If you set this property, you can access the value as long as the app is running. When your user quotes and relaunches the app, the value is gone. You should provide a default value, as you cannot assure a value exists.
How to see images in SwiftData with computed properties
You can add computed properties to your model class. In the following, I am creating a computed property that generates a ´UIImage´ from the data stored in SwiftData:
import SwiftUI
import SwiftData
@Model final public class Snippet {
...
var imageData: Data?
var image: UIImage? {
UIImage(data: imageData)
}
}
Note that with the public Xcode release you no longer need to annotate computed properties with @Transient
.
Storing data separately from your database
Large data should not be stored directly in your database because this can make your database slow. You can tell SwiftData to externally store property with the externalStorage
property option. For example, I would want to store my snippet image externally:
@Attribute(.externalStorage)
var imageData: Data?
Defining Relationships and Delete Rules in SwiftData
SwiftData will automatically determine relationships from your model classes. As an example, I want a one-to-many relationship between Snippet and Folder. Snippet has a property pointing to Folder:
@Model final public class Snippet {
var folder: Folder?
...
}
and folder has a property pointing to many Snippets that it can contain:
@Model final public class Folder {
var snippets: [Snippet]
...
}
If you use classes for your property types, SwiftData will recognise these as relationships. On the other hand, if you use value types, it uses them as stored properties.
As a second relationship, I want to have a many-to-many relationship between Tag and Snippet. Snippet has a property that points to many Tags:
@Model final public class Snippet {
var tags: [Tag] = []
...
}
and Tag has a property holding reference to many Snippets:
@Model final public class Tag {
var snippets: [Snippet] = []
...
}
Note that for the many-to-many relationship, I used non-optional arrays. If you want to use iCloud sync, you would need to change this to optional arrays.
Setting explicit inverse relationships
In some cases, you want to explicitly set what inverse property is used. In this case, you can use the @Relationship
macro like so:
@Model final public class Snippet {
@Relationship(inverse: \Tag.snippets)
var tags: [Tag]? = nil
...
}
How to set delete rules in SwiftData
With the ´@Relationship´ macro you can set delete rules for your SwiftData relationships. For example, I want to delete all the snippets in a folder, when the folder is deleted. For that, I would set a cascading delete rule like so:
@Model final public class Folder {
@Relationship(.cascade, inverse: \Snippet.folder)
var snippets: [Snippet]
...
}
The options for delete rules are the same options available for CoreData:
- cascade: A rule that deletes any related objects.
- deny: A rule that prevents the deletion of an object because it contains one or more references to other objects.
- nullify: A rule that nullifies the related object’s reference to the deleted object.
- noAction: A rule that doesn’t make changes to any related objects. You have to manually updated references to the deleted object. Otherwise, your data will be in an inconsistent state and may reference models that don’t exist.
The default delete rule is nullify.
Conclusion
In this blog post, I explored data modeling in SwiftData, a data management framework. I should you how SwiftData models data using regular Swift code. You saw how to use Schema macros like `@Model` to easily add data persistence to your data. I explore how to create initializers, and what property types are compatible with SwiftData, including optional, non-optional, custom, and enum types. The article details how to adjust model definitions using the @Attribute macro and discusses property options. Lastly, I touch on defining relationships and delete rules in SwiftData, using examples of relationships between different entities.
I find SwiftData offers many advantages over CoreData, like the optional handling. But SwiftData does not offer as many features as CoreData e.g. sectioned fetch requests. It looks to me like we are still far away from a complete and well-working version of SwiftData. If you want to use SwiftData for your future projects, it might be better to wait for iOS 18.
Further Reading:
- How to convert a CoreData project to 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
- Introduction to Data Persistence in SwiftUI with SwiftData
FAQ
What is SwiftData and how is it different from CoreData?
SwiftData is a framework introduced in WWDC 2023 that simplifies data persistence and management with a declarative approach. CoreData and SwiftUI use a lot of the same strategies. Both can use SQLight as a database. Unlike CoreData, SwiftData allows you to model your data using regular Swift code, without the need for external files or tools.
How do I define a Data Model in SwiftData?
To define a Data Model in SwiftData, you can use the @Model macro to decorate your class. SwiftData automatically creates a schema from your model file, providing it with ‘PersistentModel’ conformance.
How do I create a SwiftData Schema?
wiftData uses macros to help you define your schema quickly and easily. The most important macros are @Model, @Attribute, and @Relationship, each serving a different purpose in fine-tuning your schema.
What property types can I use with SwiftData?
In SwiftData, you can use basic types for your properties, such as String, Date, and numeric types like Int, Float, and Double. Custom types also work, if they are structs and conform to the Codable protocol.
Can I use Enums with SwiftData?
Yes, you can use enums with SwiftData as long as they conform to the Codable protocol. You can directly use enum types with your SwiftData properties.
How do I store data separately from my database in SwiftData?
In SwiftData, you can store large data separately from your database using the ´@Attribute(.externalStorage)’ macro. It tells SwiftData to store the property in a separate file from your database file.
Can I use custom types for SwiftData properties?
Yes, SwiftData supports custom types as long as they conform to the Codable protocol. For instance, you can store a color type for your model.
Great article, thank you so much for putting in the effort!
Thanks for this crucial info. Most swiftdata material seems too basic or too deep, this is perfect.
I would like to thank you for taking the time to write about SwiftData in steps with explanations. I love following Stewart Lynch’s classes, and this gave me a little more information so I could take his classes with example projects with a better understanding. I have spent a few days taking notes from you and understand this takes time to give the world. I am teaching myself SwiftUI from ground 0 because I need a skill (after becoming disabled) I can use from home to try and still have a future, and its been the hardest thing I have ever tried to do, but I won’t quit. Therefore I appreciate what you have done for us more than I can express.
Hello Karin,
Thank you very much.
It was was a great post!