NEW: Join my free 100 Days of SwiftUI challenge today! >>

How to combine Core Data and SwiftUI

Paul Hudson    @twostraws   

SwiftUI and Core Data were introduced almost exactly a decade apart – SwiftUI with iOS 13, and Core Data with iPhoneOS 3; so long ago it wasn’t even called iOS because the iPad wasn’t released yet. Despite their distance in time, Apple put in a ton of work to make sure these two powerhouse technologies work beautifully alongside each other, meaning that Core Data integrates into SwiftUI as if it were always designed that way.

First, the basics: Core Data 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: Core Data 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, Core Data implements all sorts of more advanced functionality for when you really need to lean on it: data validation, lazy loading of data, undo and redo, and much more.

In this project we’re going to be using only a small amount of Core Data’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 check the Use Core Data box, and it should have resulted in changes to your project:

  • You now have a file called Bookworm.xcdatamodeld. This describes your data model, which is effectively a list of classes and their properties.
  • There is now extra code in AppDelegate.swift and SceneDelegate.swift for setting up Core Data.

Setting up Core Data requires two steps: creating what’s called a persistent container, which is what loads and saves the actual data from device storage, and injecting that into the SwiftUI environment so that all our views can access it.

Both of these steps are already done for us by the Xcode template.

So, what remains is for us to decide what data we want to store in Core Data, and how to read it back out. To start that, we need to open Bookworm.xcdatamodeld and start describing our data using Xcode’s model editor.

Previously we described data like this:

struct Student {
    var id: UUID
    var name: String
}

However, Core Data doesn’t work like that. You see, Core Data needs to know ahead of time what all our data types look like, what it contains, and how it relates to each other. This is where the “xcdatamodeld” file comes in: we define our types as “entities”, then create properties in there as “attributes”, and Core Data is responsible for converting that into an actual database layout it can work with at runtime.

For trial purposes, please press the Add Entity button to create a new entity, then double click on its name to rename it “Student”. Next, click the + button directly below the Attributes table to add two attributes: “id” as a UUID and “name” as a string. That tells Core Data everything we need to know to create students and save them, so head back to ContentView.swift so we can write some code.

Retrieving information from Core Data is done using a fetch request – we describe what we want, how it should sorted, and whether any filters should be used, and Core Data sends back all the matching data. We need to make sure that this fetch request 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 @FetchRequest and it takes two parameters: the entity we want to query, and how we want the results to be sorted. It has quite a specific format, so let’s start by adding a fetch request for our students – please add this property to ContentView now:

@FetchRequest(entity: Student.entity(), sortDescriptors: []) var students: FetchedResults<Student>

Broken down, that creates a fetch request for our “Student” entity, applies no sorting, and places it into a property called students that has the the type FetchedResults<Student>.

From there, we can start using students like a regular Swift array, but there’s one catch as you’ll see. First, some code that puts the array into a List:

    var body: some View {
        VStack {
            List {
                ForEach(students, id: \.id) { student in
                    Text(student.name ?? "Unknown")
                }
            }
        }
    }
}

Did you spot the catch? Yes, student.name is an optional – it might have a value or it might not. This is one area of Core Data that will annoy you greatly: it has the concept of optional data, but it’s an entirely different concept to Swift’s optionals. If we say to Core Data “this thing can’t be optional” (which you can do inside the model editor), it will still generate optional Swift properties, because all Core Data cares about is that the properties have values when they are saved – they can be nil at other times.

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 store a managed object context.

Let me back up a little, because this matters. When we defined the “Student” entity, what actually happened was that Core Data created a class for us that inherits from one of its own classes: NSManagedObject. We can’t see this class in our code, because it’s generated automatically when we build our project, just like Core ML’s models. These objects are called managed because Core Data is looking after them: it loads them from the persistent container and writes their changes back too.

All our managed objects live inside a managed object context, which is the thing that’s responsible for actually fetching managed objects, as well as for saving changes and more. You can have many managed object contexts if you want, but that’s quite a way away right now – realistically you’ll be fine with one for a long time yet.

We don’t need to create this managed object context, because Xcode already made one for us. Even better, it already added it to the SwiftUI environment, which is what makes the @FetchRequest property wrapper work – it uses whatever managed object context is available in the environment.

Anyway, when it comes to adding and saving objects, we need access to the managed object context that it is in SwiftUI’s environment. This is another use for the @Environment property wrapper – we can ask it for the current managed object context, and assign it to a property for our use.

So, add this property to ContentView now:

@Environment(\.managedObjectContext) var moc

With that in place, the next step is add a button that generates random students and saves them in the managed object 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 button just below the List:

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, using the class Core Data generated for us. This needs to be attached to a managed object context, so the object knows where it should be stored. We can then assign values to it just like we normally would for a struct.

So, add these three lines to the button’s action closure now:

let student = Student(context: self.moc)
student.id = UUID()
student.name = "\(chosenFirstName) \(chosenLastName)"

Finally we need to ask our managed object context to save itself. This is a throwing function call, because in theory it might fail. In practice, nothing about what we’ve done has any chance of failing, so we can call this using try? – we don’t care about catching errors.

So, add this final line to the button’s action:

try? self.moc.save()

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 Core Data saved them.

Now, you might think this was an awful lot of learning for not a lot of result, but you now know what entities and attributes are, you know what managed objects and fetch requests are, and you’ve seen how to save changes. We’ll be looking at Core Data 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 reset your code back to its initial state, and make sure you delete the Student entity from our data model – we don’t need it any more.

LEARN SWIFTUI FOR FREE I have a massive, free SwiftUI video collection on YouTube teaching you how to build complete apps with SwiftUI – check it out!

BUY OUR BOOKS
Buy Pro Swift 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 (Vapor Edition) 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 Server-Side Swift (Kitura Edition) Buy Beyond Code

Was this page useful? Let us know!

Average rating: 5.0/5