Introducing SwiftData the successor of CoreData

francescoleoni98

Francesco Leoni

Posted on June 14, 2023

Introducing SwiftData the successor of CoreData

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
  }
Enter fullscreen mode Exit fullscreen mode
  1. Create a model class that contains all the properties you wish to persist
  2. Mark the classes with the Macro @Model
  3. 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 the Note class and add @Transient() to notes 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
Enter fullscreen mode Exit fullscreen mode

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] = []
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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]) // <-
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
francescoleoni98
Francesco Leoni

Posted on June 14, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related