UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

Introduction to SwiftData and SwiftUI

Paul Hudson    @twostraws   

SwiftUI is a powerful, modern framework for building great apps on all of Apple's platforms, and SwiftData is a powerful, modern framework for storing, querying, and filtering data. Wouldn't it be nice if they just fitted together somehow?

Well, not only do they work together brilliantly, but they take such little code that you'll barely believe the results – you can create remarkable things in just a few minutes.

First, the basics: SwiftData is an object graph and persistence framework, which is a fancy way of saying it lets us define objects and properties of those objects, then lets us read and write them from permanent storage.

On the surface this sounds like using Codable and UserDefaults, but it’s much more advanced than that: SwiftData is capable of sorting and filtering of our data, and can work with much larger data – there’s effectively no limit to how much data it can store. Even better, SwiftData implements all sorts of more advanced functionality for when you really need to lean on it: iCloud syncing, lazy loading of data, undo and redo, and much more.

In this project we’re going to be using only a small amount of SwiftData’s power, but that will expand soon enough – I just want to give you a taste of it at first.

When you created your Xcode project I asked you to not enable SwiftData support, because although it gets some of the boring set up code out of the way it also adds a whole bunch of extra example code that is just pointless and just needs to be deleted.

So, instead you’re going to learn how to set up SwiftData by hand. It takes three steps, starting with us defining the data we want to use in our app.

Previously we described data by creating a Swift file called something like Student.swift, then giving it this code:

@Observable
class Student {
    var id: UUID
    var name: String

    init(id: UUID, name: String) {
        self.id = id
        self.name = name
    }
}

We can turn that into a SwiftData object – something that it can save in its database, sync with iCloud, search, sort, and more – by making two very small changes.

First we need to add another import at the top of the file:

import SwiftData

That tells Swift we want to bring in all the functionality from SwiftData.

And now we want to change this:

@Observable
class Student {

To this:

@Model
class Student {

…and that's it. That's literally all it takes to give SwiftData all the information it needs to load and save students. It can also now query them, delete them, link them with other objects, and more.

This class is called a SwiftData model: it defines some kind of data we want to work with in our apps. Behind the scenes, @Model builds on top of the same observation system that @Observable uses, which means it works really well with SwiftUI.

Now that we've defined the data we want to work with, we can proceed to the second step of setting up SwiftData: writing a little Swift code to load that model. This code will tell SwiftData to prepare some storage for us on the iPhone, which is where it will read and write Student objects.

This work is best done in the App struct. Every project has one of these, including all the projects we've made so far, and it acts as the launch pad for the whole app we're running.

As this project is called Bookworm, our App struct will be inside the file BookwormApp.swift. It should look like this:

import SwiftUI

@main
struct BookwormApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

You can see it looks a bit like our regular view code: we still have an import SwiftUI, we still use a struct to create a custom type, and there's our ContentView right there. The rest is new, and really we care about two parts:

  1. The @main line tells Swift this is what launches our app. Internally this is what bootstraps the whole program when the user launches our app from the iOS Home Screen.
  2. The WindowGroup part tells SwiftUI that our app can be displayed in many windows. This doesn't do much on iPhone, but on iPad and macOS it becomes a lot more important.

This is where we need to tell SwiftData to setup all its storage for us to use, which again takes two very small changes.

First, we need to add import SwiftData next to import SwiftUI. I'm a big fan of sorting my imports alphabetically, but it's not required.

Second, we need to add a modifier to the WindowGroup so that SwiftData is available everywhere in our app:

.modelContainer(for: Student.self)    

A model container is SwiftData's name for where it stores its data. The first time your app runs this means SwiftData has to create the underlying database file, but in future runs it will load the database it made previously.

At this point you've seen how to create data models using @Model, and you've sent how to create a model container using the modelContainer() modifier. The third part of the puzzle is called the model context, which is effectively the “live” version of your data – when you load objects and change them, those changes only exist in memory until they are saved. So, the job of the model context is to let us work with all our data in memory, which is much faster than constantly reading and writing data to disk.

Every SwiftData app needs a model context to work with, and we've already created ours – it's created automatically when we use the modelContainer() modifier. SwiftData automatically creates one model context for us, called the main context, and stores it in SwiftUI's environment,

That completes all our SwiftData configuration, so now it's time for the fun part: reading data, and writing it too.

Retrieving information from SwiftData is done using a query – we describe what we want, how it should sorted, and whether any filters should be used, and SwiftData sends back all the matching data. We need to make sure that this query stays up to date over time, so that as students are created or removed our UI stays synchronized.

SwiftUI has a solution for this, and – you guessed it – it’s another property wrapper. This time it’s called @Query and it's available as soon as you add import SwiftData to a file.

So, add an import for SwiftData at the top of ContentView.swift, then add this property to the ContentView struct:

@Query var students: [Student]

That looks like a regular Student array, but just adding @Query to the start is enough to make SwiftData loads students from its model container – it automatically finds the main context that was placed into the environment, and queries the container through there. We haven't specified which students to load, or how to sort the results, so we'll just get all of them.

From there, we can start using students like a regular Swift array – put this code into your view body:

NavigationStack {
    List(students) { student in
        Text(student.name)
    }
    .navigationTitle("Classroom")
}

You can run the code if you want to, but there isn’t really much point – the list will be empty because we haven’t added any data yet, so our database is empty. To fix that we’re going to create a button below our list that adds a new random student every time it’s tapped, but first we need a new property to access the model context that was created earlier.

Add this property to ContentView now:

@Environment(\.modelContext) var modelContext

With that in place, the next step is add a button that generates random students and saves them in the model context. To help the students stand out, we’ll assign random names by creating firstNames and lastNames arrays, then using randomElement() to pick one of each.

Start by adding this toolbar to the your List:

.toolbar {
    Button("Add") {
        let firstNames = ["Ginny", "Harry", "Hermione", "Luna", "Ron"]
        let lastNames = ["Granger", "Lovegood", "Potter", "Weasley"]

        let chosenFirstName = firstNames.randomElement()!
        let chosenLastName = lastNames.randomElement()!

        // more code to come
    }
}

Note: Inevitably there are people that will complain about me force unwrapping those calls to randomElement(), but we literally just hand-created the arrays to have values – it will always succeed. If you desperately hate force unwraps, perhaps replace them with nil coalescing and default values.

Now for the interesting part: we’re going to create a Student object. Add this in place of the // more code to come comment:

let student = Student(id: UUID(), name: "\(chosenFirstName) \(chosenLastName)")

Finally we need to ask our model context to add that student, which means it will be saved. Add this final line to the button’s action:

modelContext.insert(student)

At last, you should now be able to run the app and try it out – click the Add button a few times to generate some random students, and you should see them slide somewhere into our list. Even better, if you relaunch the app you’ll find your students are still there, because SwiftData automatically saved them.

Now, you might think this was an awful lot of learning for not a lot of result, but you now know what models, model containers, and model contexts are, and you've seen how to insert and query data. We’ll be looking at SwiftData more later on in this project, as well in the future, but for now you’ve come far.

This was the last part of the overview for this project, so please go ahead and reset your project ready for the real work to begin. That means resetting ContentView.swift, BookwormApp.swift, and also deleting Student.swift.

TAKE YOUR SKILLS TO THE NEXT LEVEL If you like Hacking with Swift, you'll love Hacking with Swift+ – it's my premium service where you can learn advanced Swift and SwiftUI, functional programming, algorithms, and more. Plus it comes with stacks of benefits, including monthly live streams, downloadable projects, a 20% discount on all books, and free gifts!

Find out more

Sponsor Hacking with Swift and reach the world's largest Swift community!

BUY OUR BOOKS
Buy Pro Swift Buy Pro SwiftUI Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let us know!

Average rating: 4.8/5

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.