SwiftData the Successor of CoreData Explained with SwiftUI

Discover the SwiftData framework built on top of CoreData. Save and fetch data locally. Available for Xcode 15, Swift and SwiftUI.

Francesco Leoni

5 min read

Availability

iOS 17

macOS 14

watchOS 10

tvOS 17

Xcode 15

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

}

  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

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.

If you have any question about this article, feel free to email me or tweet me @franceleonidev and share your opinion.

Thank you for reading and see you in the next article!

Share this article

Related articles


Combine CoreData and SwiftUI

See how to use CoreData database with SwiftUI. Syncing changes from CoreData to every View of your app.

5 min read

SwiftUICoreData

From REST API to CoreData in One Step

See how to convert fetched data from a REST API to entities and persist them to CoreData, the built-in local database of Apple.

3 min read

REST APICoreData

Make your Chart Scrollable with SwiftUI Charts (iOS 17)

Discover the new SwiftUI Charts APIs that enables you to create scrollable chart easily. Available for Xcode 15 and iOS 17.

2 min read

ChartsSwiftUI