Getting started with SwiftData

Getting started with SwiftData

By Atikur Rahman , Lead iOS developer at Evangelist Apps.

At WWDC 2023, Apple announced a new framework for persisting our app data — the SwiftData framework. SwiftData uses the proven storage architecture of Core Data, but with a simpler and more expressive syntax that leverages Swift’s modern features. It lets us write our model code declaratively.

SwiftData works by using Swift macros to transform our existing classes into stored models that are managed by Core Data. SwiftData also provides a model context and a model container that handle the storage and syncing of our data. SwiftData integrates seamlessly with SwiftUI, allowing us to use the @Query property wrapper to display data in our views, and use predicates to filter and sort data using regular Swift code.

In this article, we will learn how to use SwiftData framework by building a simple Todo list app.

  1. Let’s create a new iOS app using Xcode 15 beta.
  2. Create a model class called TodoItem -

import Foundation
import SwiftData

@Model
class TodoItem: Identifiable {
    var id: UUID
    var title: String
    var isDone: Bool
    
    init(title: String, isDone: Bool = false) {
        self.id = UUID()
        self.title = title
        self.isDone = isDone
    }
}        

This is just a regular model class, with just two changes — first we import the SwiftData framework and then we annotate the class with Model macro.

In order for SwiftData to manage and store a model class, we need to annotate the class with Model macro. The macro makes the class conform to the PersistentModel protocol, which SwiftData uses to inspect the class and create an internal schema. The macro also makes the class conform to the Observable protocol, which SwiftData uses to track changes in the class. We can also use other macros such as Attribute and Relationship to customize the behavior of our model’s properties. But to keep things simple, we will not discuss about those in this article.

3. The next step is to tell SwiftData which models we want to store, so that it can create the appropriate schema for our models. This is handled by Model Container. This can be easily done by using the modelContainer view modifier. Let’s add this view modifier at the very top of our view hierarchy so all nested views inherit the properly configured environment. Open the TodoListApp.swift file and add the view modifier to the ContentView. Also make sure to import SwiftData.

import SwiftUI
import SwiftData

@main
struct TodoListApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .modelContainer(for: TodoItem.self)
        }
    }
}        

4. We use a model context to manage instance of our model classes at runtime. This enables us to fetch, insert, edit or delete operations of model instances and persist that data. We can get model context using the environment variable. We will see how to do that shortly.

We use Query property wrapper to fetch model data & perform actions on them. This Query property wrapper helps us to keep data in sync with the underlying persistent storage. Query takes a predicate or a fetch descriptor as a parameter and performs the fetch when the view appears. Query also tells SwiftUI about any changes to the fetched models so the view can update accordingly.

Let’s update ContentView with following code -

import SwiftUI
import SwiftData

struct ContentView: View {
    @State private var newItemTitle = ""
    @Environment(\.modelContext) private var context
    @Query(sort: \.title, order: .forward) var todos: [TodoItem]
    
    var body: some View {
        NavigationView {
            VStack {
                TextField("Enter new item", text: $newItemTitle)
                    .textFieldStyle(.roundedBorder)
                    .padding()
                Button("Add Item", action: addNewItem)
                
                List(todos) { item in
                    TodoItemView(item: item)
                }
            }
            .navigationTitle("Todo List")
        }
    }
    
    private func addNewItem() {
        context.insert(TodoItem(title: newItemTitle))
        newItemTitle = ""
    }
}        

Here we get model context using the environment variable and use Query property wrapper to get an array of TodoItem.

@Environment(\.modelContext) private var context
@Query(sort: \.title, order: .forward) var todos: [TodoItem]        

Our view is quite simple — we have a text field to enter title for new todo item and a button for adding the item.

Our view also has a list, which iterates through the items fetched using the Query and then show a TodoItemView for each item -

List(todos) { item in
    TodoItemView(item: item)
}        

We will write the code for TodoItemView in a bit, but first let’s see how to add a new todo item and save it to persistent storage using SwiftData. When Add Item button is tapped, the following code is executed -

private func addNewItem() {
    context.insert(TodoItem(title: newItemTitle))
    newItemTitle = ""
}        

Here, we create a new TodoItem using the text entered into the text field and then call insert method of context. When we insert the model instance into the context, we enable SwiftData to persist that model instance and track changes to it. context periodically checks whether it contains unsaved changes, and if there is any, it will automatically save it.

5. Finally, let’s create a view for individual todo item, called TodoItemView -

import SwiftUI

struct TodoItemView: View {
    @Environment(\.modelContext) private var context
    var item: TodoItem
    
    var body: some View {
        HStack {
            Image(systemName: item.isDone ? "checkmark.circle.fill" : "circle")
                .foregroundColor(item.isDone ? .green : .gray)
                .onTapGesture {
                    item.isDone.toggle()
                }
            Text(item.title)
                .strikethrough(item.isDone)
            Spacer()
            Image(systemName: "trash")
                .foregroundColor(.red)
                .onTapGesture {
                    context.delete(item)
                }
        }
        .padding()
    }
}        

This will render a simple UI with 3 elements — the title of the todo item, a button on the left to toggle the completion status of the todo item and the trash button on the right to delete the todo item.

To toggle the completion status of the todo item, we simply toggle the isDone property of the TodoItem -

item.isDone.toggle()        

Since item (instance of TodoItem) is fetched using Query property wrapper from the ContentView and passed into TodoItemView, any changes made to item will be tracked by the context automatically.

To delete the item, we call the delete method of context and pass the todo item as follows -

context.delete(item)        

Now you can run the app on your simulator and add few todo items, mark few of them as done, delete few etc. If you stop the simulator and run the app again, you will see the changes you made persisted.

That’s it for now. In this article, we have introduced you to SwiftData, a new framework that lets you write your model code declaratively to add managed persistence. We have shown you how to use SwiftData to define your model classes using macros, use the model context to insert, update, and delete model instances and use the query property wrapper to display your model data in SwiftUI views.

Thanks for reading.

Hope you enjoyed reading this article.

Cheers!

#swift #wwdc2023 #ios17 #swiftui #ios

It would be better to add @Attribute(.unique)?to id in TodoItem class.

回复

要查看或添加评论,请登录

Evangelist Apps的更多文章

社区洞察

其他会员也浏览了