DEV Community

Dean Thompson
Dean Thompson

Posted on • Updated on

[WWDC2023] SwiftData Basic Use Case in 6 Steps

SwiftData

It has happened! The long-awaited successor to CoreData has been released—SwiftData! Although SwiftData retains some of CoreData's characteristics (uses the same storage architecture), it does enable us to persist data using simple, declarative code. It seems like it may be a game changer for Swift developers, so I immediately had to check it out.

As I delved into basic use cases for SwiftData, like To-Do apps, I noticed that the use of SwiftData comes down to around 6 steps. In this article, I will guide you through the process of building a simple To-Do App using SwiftData, shedding light on these basic steps. The article will not cover the app's UI implementation, as the emphasis is on SwiftData.

Here are the six steps we'll cover:

  1. import SwiftData.
  2. Create your model: Define a class for your data.
  3. Set your model to the modelContainer: This lets SwiftData know what data it will be working with.
  4. Declare @Environment(\.modelContext) in your view: This establishes the context in which your data will exist and be manipulated.
  5. Prefix @Query to an array variable: A wrapper to access your stored data.
  6. Interact with your context for CRUD operations.

Let's get started by diving into each of these steps in more detail.

1. Import SwiftData

The first step is to import SwiftData. Yes, I know it is obvious, but we all forget sometimes.

import SwiftData
Enter fullscreen mode Exit fullscreen mode

2. Create your Model

In past, when using CoreData, you would open a .xcdatamodel file and implement an Entity by adding it with "Add Entity". In SwiftData we simply model data with ordinary Swift types using @Model. Here, I placed the @Model wrapper above my data class ToDoItem. This model will represent the individual tasks in our To-Do list. Each task will have a title, content, and the date it was added.

@Model
class ToDoItem {
    let title: String
    let content: String
    let dateAdded: Date

    init(title: String, content: String, dateAdded: Date = .init()) {
        self.title = title
        self.content = content
        self.dateAdded = dateAdded
    }
}
Enter fullscreen mode Exit fullscreen mode

3. Set .modelContainer() in App

In SwiftData, the .modelContainer() is used to establish the storage area or "container" for your data model within your application. Think of it as designating a specific space within your app to store all the data instances of the ToDoItem model.

The container is set within the main app structure (in this case, SwiftDataToDoExampleApp because this ensures the data is accessible throughout your entire application.

@main
struct SwiftDataToDoExampleApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(for: ToDoItem.self)
    }
}
Enter fullscreen mode Exit fullscreen mode

In this code snippet, .modelContainer(for: ToDoItem.self) is added to WindowGroup. This tells the application to create a storage area specifically for ToDoItem instances. Now, wherever we are in the app, we can manipulate our to-do items.

4. Declare @Environment(\.modelContext) in your view

Next, declare @Environment(.modelContext) within your View. @Environment(\.modelContext) allows us to read from our container. It sounds a bit complex, but it's essentially a way to establish a connection between your View and the data stored in your modelContainer from step 3. In other words, It acts like a portal, that our View can use to interact with the modelContainer.

struct ContentView: View {
    @Environment(\.modelContext) var context
    // ...
}
Enter fullscreen mode Exit fullscreen mode

5. Prefix @Query to an array variable

In SwiftUI view, you can fetch data using the wrapper @Query. SwiftData and SwiftUI work in together, and when the underlying data changes, the view is automatically updated with the most recent data.

@Query var items: [ToDoItem]
Enter fullscreen mode Exit fullscreen mode

Just like in CoreData, you can add SortDescriptor and Predicate to a fetch. In the case below, the ToDoItem's will be displayed in the reverse order of when they were added.

@Query(FetchDescriptor(sortBy: [SortDescriptor(\.dateAdded, order: .reverse)]),animation: .snappy) private var items: [ToDoItem]
Enter fullscreen mode Exit fullscreen mode

6. Interact with your context for CRUD operations

Now that we have completed the set up of our model, container, context, and the way to communicate with the view, it is time to interact with our context. The first interaction with the context will be inserting data(ToDoItem) into the context. After inserting a ToDoItem into the context, don't forget to save it. This is a two-step process, and the changes made to the context are finalized by saving.

In the snippet below I have created two functions one to insert the item and one to then save it into the context.

func insert(_ item: ToDoItem) {
    context.insert(item)
    save()
}

func save() {
    do {
        try context.save()
    } catch {
        print(error.localizedDescription)
    }
}
Enter fullscreen mode Exit fullscreen mode

The same goes for deletion. Delete from the context and then save to the context to make sure it has been updated.

func delete(_ item: ToDoItem) {
    context.delete(item)
    save()
} 
Enter fullscreen mode Exit fullscreen mode

And with the above 6 steps, we have implemented SwiftData into our To-do app. I will include all of the UI code below so that you are able to see how I put together the whole project. Feel free to copy and paste it into Xcode.

Finally, remember that SwiftData is still in beta, and it can randomly crash, especially after saving. These issues will be updated as Xcode 15 comes out of beta, I imagine.

App Screenshot

App Preview Gif

The Code

ContentView.swift

import SwiftUI
import SwiftData

struct ContentView: View {
    @Environment(\.modelContext) var context
    @Query var items: [ToDoItem]

    @State private var showingSheet: Bool = false
    @State private var title: String = ""
    @State private var content: String = ""

    var body: some View {
        NavigationStack {
            List {
                ForEach(items)  { item in
                    VStack(alignment: .leading) {
                        HStack(alignment: .top) {

                            Text(item.title)
                                .bold()

                            Spacer()

                            Text(item.dateAdded.formatted(date: .numeric, time: .shortened))
                                .font(.caption)
                                .foregroundStyle(.gray)

                        }

                        Text(item.content)
                            .font(.subheadline)
                    }
                    .swipeActions(content: {
                        Button(action: {
                            self.delete(item)
                        }, label: {
                            Label("Delete", systemImage: "xmark.bin.fill")
                        })
                        .tint(Color.red)
                    })
                }
            }
            .navigationTitle("To Do List")
            .toolbar {
                ToolbarItem(placement: .topBarTrailing) {
                    Button {
                        showingSheet.toggle()
                    } label: {
                        Image(systemName: "plus")
                    }

                }
            }
            .sheet(isPresented: $showingSheet, content: {
                Form {
                    Section {
                        TextField("Add the title...", text: $title)
                    } header: {
                        Text("Title")
                    }

                    Section {
                        TextEditor(text: $content)
                    } header: {
                        Text("Content / Notes")
                    }

                    Section {
                        Button("Add Item") {
                            let item = ToDoItem(title: title, content: content)
                            insert(item)

                            showingSheet = false
                            title = ""
                            content = ""
                        }
                        .disabled(title.isEmpty || content.isEmpty)
                    }
                }
            })
        }
    }

    func insert(_ item: ToDoItem) {
        context.insert(item)
        save()
    }

    func delete(_ item: ToDoItem) {
        context.delete(item)
        save()
    }

    func save() {
        do {
            try context.save()
        } catch {
            print(error.localizedDescription)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

ToDoItem.swift

import SwiftUI
import SwiftData

@Model
class ToDoItem {
    let title: String
    let content: String
    let dateAdded: Date

    init(title: String, content: String, dateAdded: Date = .init()) {
        self.title = title
        self.content = content
        self.dateAdded = dateAdded
    }
}
Enter fullscreen mode Exit fullscreen mode

SwiftDataToDoExampleApp.swift

import SwiftUI

@main
struct SwiftDataToDoExampleApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(for: ToDoItem.self)
    }
}
Enter fullscreen mode Exit fullscreen mode

Thanks for checking out the post.
All the best!

Dean Thompson

Follow me!
LinkedIn
Twitter
Instagram

References

Apple's Press Release

Great example using SwiftData from KavSoft

Great article by Alexander Logan. Also goes into depth on using SwiftData with relationships: Alexander Logan's Blog

Top comments (0)