Database · · 8 min read

Getting Started with SwiftData for SwiftUI Development

Getting Started with SwiftData for SwiftUI Development

One common question in SwiftUI app development is how to work with Core Data to save data permanently in the built-in database. Despite Apple’s ongoing efforts to simplify the APIs of Core Data, new comers often find the framework challenging to use. However, there is good news on the horizon. Apple will be releasing a new framework called SwiftData in iOS 17 to replace Core Data. SwiftData is designed to be much easier to use for data modelling and management, offering a more user-friendly approach.

What’s SwiftData

First and foremost, it’s important to note that the SwiftData framework should not be confused with a database. Built on top of Core Data, SwiftData is actually a framework designed to help developers manage and interact with data on a persistent store. While the default persistent store for iOS is typically the SQLite database, it’s worth noting that persistent stores can take other forms as well. For example, Core Data can also be used to manage data in a local file, such as an XML file.

Regardless of whether you’re using Core Data or the SwiftData framework, both tools serve to shield developers from the complexities of the underlying persistent store. Consider the SQLite database, for instance. With SwiftData, there’s no need to worry about connecting to the database or understanding SQL in order to retrieve data records. Instead, developers can focus on working with APIs and Swift Macros, such as @Query and @Model, to effectively manage data in their applications.

The SwiftData framework is newly introduced in iOS 17 to replace the previous framework called Core Data. Core Data has long been the data management APIs for iOS development since the era of Objective-C. Even though developers can integrate the framework into Swift projects, Core Data is not a native solution for both Swift and SwiftUI.

In iOS 17, Apple finally introduces a native framework called SwiftData for Swift on persistent data management and data modeling. It’s built on top of Core Data but the APIs are completely redesigned to make the most out of Swift.

Using Code to Create the Data Model

swiftui-coredata-model-editor

If you have used Core Data before, you may remember that you have to create a data model (with a file extension .xcdatamodeld) using a data model editor for data persistence. With the release of SwiftData, you no longer need to do that. SwiftData streamlines the whole process with macros, another new Swift feature in iOS 17. Say, for example, you already define a model class for Song as follows:

class Song {
  var title: String
  var artist: String
  var album: String
  var genre: String
  var rating: Double
}

To use SwiftData, the new @Model macro is the key for storing persistent data using SwiftUI. Instead of building the data model with model editor, SwiftData just requires you to annotate the model class with the @Model macro like this:

@Model class Song {
  var title: String
  var artist: String
  var album: String
  var genre: String
  var rating: Double
}

This is how you define the schema of the data model in code. With this simple keyword, SwiftData automatically enables persistence for the data class and offers other data management functionalities such as iCloud sync. Attributes are inferred from properties and it supports basic value types such as Int and String.

SwiftData allows you to customize how your schema is built using property metadata. You can add uniqueness constraints by using the @Attribute annotation, and delete propagation rules with the @Relationship annotation. If there are certain properties you do not want included, you can use the @Transient macro to tell SwiftData to exclude them. Here is an example:

@Model class Album {
  @Attribute(.unique) var name: String
  var artist: String
  var genre: String

  // The cascade relationship instructs SwiftData to delete all 
    // songs when the album is deleted.
  @Attribute(.cascade) var songs: [Song]? = []
}

To drive the data persistent operations, there are two key objects of SwiftData that you should be familiar with: ModelContainer and ModelContext. The ModelContainer serves as the persistent backend for your model types. To create a ModelContainer, you simply need to instantiate an instance of it.

// Basic
let container = try ModelContainer(for: [Song.self, Album.self])

// With configuration
let container = try ModelContainer(for: [Song.self, Album.self], 
                                    configurations: ModelConfiguration(url: URL("path"))))

In SwiftUI, you can set up the model container at the root of the application:

import SwiftData
import SwiftUI

@main
struct MusicApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer (for: [Song.self, Album.self]))
    }
}

Once you have set up the model container, you can begin using the model context to fetch and save data. The context serves as your interface for tracking updates, fetching data, saving changes, and even undoing those changes. When working with SwiftUI, you can typically obtain the model context from your view’s environment:

struct ContextView: View {
    @Environment(\.modelContext) private var modelContext
}

With the context, you are ready to fetch data. The simplest way is to use the @Query property wrapper. You can easily load and filter anything stored in your database with a single line of code.

@Query(sort: \.artist, order: .reverse) var songs: [Song]

To insert item in the persistent store, you can call the insert method of the model context and pass it the model objects to insert.

modelContext.insert(song)

Similarly, you can delete the item via the model context like this:

modelContext.delete(song)

This is a brief introduction of SwiftData. If you’re still feeling confused about how to use SwiftData? No worries. You will understand its usage after building a ToDO app.

Building a Simple To Do App

Now that you have a basic understanding of SwiftData, I would like to demonstrate how to build a simple to-do app using this framework. Please note that the app is not fully functional and only allows users to add a random task to the to-do list. However, it serves as a good starting point to familiarize yourself with the SwiftData framework.

swiftui-swiftdata-todo-app

Assuming you’ve created a SwiftUI project in Xcode, let’s first create the data model of the app. Create a new file named ToDoItem and update the content like this:

import Foundation
import SwiftData

@Model class ToDoItem: Identifiable {
    var id: UUID
    var name: String
    var isComplete: Bool

    init(id: UUID = UUID(), name: String = "", isComplete: Bool = false) {
        self.id = id
        self.name = name
        self.isComplete = isComplete
    }
}

As discussed earlier, SwiftData simplifies the process of defining a schema using code. All you need to do is annotate the model class with the @Model macro. SwiftData will then automatically enable persistence for the data class.

Before we move onto building the UI of the app and handling the data persistent, let’s create a helper function for generating a random to-do item:

func generateRandomTodoItem() -> ToDoItem {
    let tasks = [ "Buy groceries", "Finish homework", "Go for a run", "Practice Yoga", "Read a book", "Write a blog post", "Clean the house", "Walk the dog", "Attend a meeting" ]

    let randomIndex = Int.random(in: 0..<tasks.count)
    let randomTask = tasks[randomIndex]

    return ToDoItem(name: randomTask, isComplete: Bool.random())
}

Next, let’s build the main UI of the to-do app. In the ContentView.swift file, update the code like this:

import SwiftData

struct ContentView: View {
    @Query var todoItems: [ToDoItem]

    var body: some View {
        NavigationStack {
            List {
                ForEach(todoItems) { todoItem in
                    HStack {
                        Text(todoItem.name)

                        Spacer()

                        if todoItem.isComplete {
                            Image(systemName: "checkmark")
                        }
                    }
                }
            }

            .navigationTitle("To Do List")
        }
    }
}

We mark the todoItems array with the @Query property wrapper. This @Query property automatically fetches the required data for you. In the provided code, we specify to fetch the ToDoItem instances. Once we retrieve the to-do items, we utilize the List view to display the items.

Set up the model container

To drive the data persistent operations, we also need to set up the model container. Switch over to ToDoDemoAppApp.swift and attach the modelContainer modifier like this:

struct ToDoDemoAppApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(for: ToDoItem.self)
    }
}

Here, we set a shared model container for storing instances of ToDoItem.

If you preview the ContentView, the list view is empty. Obviously, we haven’t stored any to-do items in the database. Now, let’s add a “Add item” button to insert a random to-do item into the database.

Storing to-do items into the database

In ContentView.swift, declare the following variable to retrieve the model context:

@Environment(\.modelContext) private var modelContext

After obtaining the model context, we can easily insert data into the database. We’ll add a toolbar button for adding a random to-do item. Insert the following code inside the NavigationStack view (place it after navigationTitle):

.toolbar {
    Button("", systemImage: "plus") {
        modelContext.insert(generateRandomTodoItem())
    }
}

To store an item into database, you simply call the insert method of the model context.

swiftui-todo-list-model-context

Now you’re ready to test the app in the simulator. However, if you intend to test it in the preview canvas, you need to make one additional modification by adding the model container within the #Preview block:

#Preview {
    ContentView()
        .modelContainer(for: ToDoItem.self)
}

When you tap the “+” button, the app instantly stores the to-do item. Simultaneously, it retrieves the new item from the database and displays it in the list view.

Updating an existing item

SwiftData significantly reduces the amount of work required to handle item updates or modifications in the persistent store. By simply marking your model objects with the @Model macro, SwiftData automatically modifies the setters for change tracking and observation. This means that no code changes are needed to update the to-do items.

To test the update behavior, you can simply run the app on a simulator. When you tap a to-do item, it should be marked as complete. This change is now saved permanently in the device’s database. Even after restarting the app, all the items will still be retained.

Deleting the item from the database

Now that you know how to perform fetch, update, and insert, how about data deletion? We will add a feature to the app for removing a to-do item.

In the ContentView struct, attach the onDelete modifier to the ForEach loop:

.onDelete(perform: { indexSet in
    for index in indexSet {
        let itemToDelete = todoItems[index]
        modelContext.delete(itemToDelete)
    }
})

This closure takes an index set that stores the indices of the items to be deleted. To remove an item from the persistent store, simply call the delete function of the model context and specify the item to be deleted.

The onDelete modifier automatically enables the swipe-to-delete feature in the list view. To try this out, simply run the app and swipe to delete an item. This will completely remove the item from the database.

Summary

I hope that you now have a better understanding of how to integrate SwiftData into a SwiftUI project and how to perform all basic CRUD (create, read, update & delete) operations. Apple has put a lot of efforts to make persistent data management and data modeling easier for Swift developers and new comers.

While Core Data remains an option for backward compatibility, it’s time to learn the SwiftData framework, especially if you are developing an app exclusively for iOS 17 or later. Embrace this new framework to leverage the enhanced capabilities and benefits SwiftData offers.

Read next