Core Data and SwiftUI 2.0 – Saving, retrieving, updating and deleting persistent data

Share on facebook
Share on twitter
Share on pinterest
Updated for Xcode 12.0 and SwiftUI 2.0 ✅

Hello, and welcome to a new tutorial! Today we will learn how to use the Core Data framework with SwiftUI to store and manage persistent data. The integration of Core Data into SwiftUI projects is surprisingly easy. By creating a useful app for a small pizza restaurant, we will talk through all basic CRUD operations (Create, Read, Update, and Delete Data) used in Core Data.

In this tutorial, we will explore:

  • How Core Data and SwiftUI work together
  • Creating and updating Core Data objects
  • How to update views when stored data gets updated
  • Using SwiftUI property wrappers for fetching Core Data objects

We will create a simple app for a pizza restaurant that waiters can use to take and manage orders.

The finished app will look like this:

Setting up Core Data using SwiftUI and Xcode 12

To get started, open Xcode 12 and create a new “App” under “Multiplatform” or “iOS”. You can name your project however you want, for instance, “PizzaRestaurant”. But make sure to use SwiftUI as the “Interface” mode and SwiftUI App as the “Life Cycle” mode. Also, make sure that you check the “Use Core Data” box. This will automatically set up the initial Core Data implementation for our app!

Make sure you “reset” the ContentView by removing the generated code from it since we won’t need it for our app.

import SwiftUI
import CoreData

struct ContentView: View {
    var body: some View {
        Text("Hello World!")
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

In the project navigator, you can spot the “.xcdatamodeld” file Xcode created for us. In this file, we set up and manage the Entities of our Core Data data model for our SwiftUI app. If you are not familiar with the concept of Entities: You can think of an entity as a class, and an attribute, as a property of that class. The only Entity we need for our app is for holding the different orders. Delete the default “Item” Entity and create a new one by clicking on the large plus button at the bottom and then double-click on the created Entity to rename it to “Order”.

We need to know the following information about each order: The type of pizza the customer ordered, how many slices he wants to eat, and the number of the table the customer is sitting at. Each order should also have a unique “id” and a “status” attribute for keeping track of whether the order is already completed. For the id, we use the UUID type (this automatically creates a unique id for us). For the numberOfSclices we select Integer16 and for the rest String.

How Core Data works in SwiftUI and Xcode 12

That’s it! We just finished setting up a basic Core Data model for holding the orders for our pizza restaurant app. Wasn’t that easy? Let’s take a look at how CoreData was implemented into our SwiftUI project by checking the “Use CoreData” box earlier. 

To look behind the scenes, open the PizzaRestaurantApp.swift file. You already know that the App struct primarily handles booting up the initial view, which is the ContentView by default. Because we checked “Use CoreData” when creating our project earlier, Xcode created a property called persistenceController and applied an important modifier to the launched ContentView.

Let’s take a look at persistenceController property first.

let persistenceController = PersistenceController.shared

This property is assigned to a PersistenceController. We can find this PersistenceController in the Persistence.swift file. The PersistenceController struct contained in this file includes various properties.

Let us briefly review the most important ones. The preview property allows us to use the CoreData functionality inside preview simulators. 

Note: Since we have deleted the default “Item” Entity and created a new one called “Order”, we have to make a quick adjustment here. Delete the existing for-in loop and insert the following loops instead.

static var preview: PersistenceController = {
        //...
        for _ in 0..<10 {
            let newItem = Order(context: viewContext)
            newItem.status = "pending"
            newItem.id = UUID()
            newItem.tableNumber = "12"
            newItem.pizzaType = "Margherita"
            newItem.numberOfSlices = 4
        }
        //...
    }()

The container property is the heart of the PersistenceController, which performs many different operations for us in the background when we store and call data. Most importantly, the container allows us to access the so-called viewContext, which serves as in an in-memory scratchpad where objects are created, fetched, updated, deleted, and saved back to the persistent store of the device where the app runs on.

The container gets initialized within the PersistenceController’s init function. In this, the container property gets assigned to an NSPersistentContainer instance. We need to use the name of our “.xcdatamodeld” file, which is “Shared” (or *YourAppName* when you created a mere iOS App project), as the “name” argument.

init(inMemory: Bool = false) {
        container = NSPersistentContainer(name: "PizzaRestaurant")
        //...
    }

Now, let’s take a look at the .environment modifier applied to the ContentView in our PizzaRestaurantApp struct.

ContentView()
    .environment(\.managedObjectContext, persistenceController.container.viewContext)

What does this .environment modifier do? Before our ContentView gets launched as the root view, it feeds the environment’s managedObjectContext key with the viewContext we just talked about.

The “environment” is where system-wide settings are saved, for instance, Calendar, Locale, ColorScheme, and now, also the viewContext contained in the persistenceController’s container property. Each of these settings has its own key; in our case, it’s the .managedObjectContext key.

Now, every view in our app can use the viewContext as a “scratchpad” to retrieve, update, and store objects. We simply need to use the managedObjectContext environment key for accessing it, as you will see later on.

Don’t worry if you are not familiar with this. The only thing you need to remember is that we can use the managedObjectContext for fetching and saving our orders. You’ll see how easy this is in a moment.

But first, we make a small adjustment to our CoreData data model.

Customizing our data model 🛠

Let’s hold on a second and reconsider choosing String as the status attribute’s type. Each order’s status should only be “Pending”, “Preparing” and “Completed”. Wouldn’t be using an enum the better choice for this? Unfortunately, we can’t create and use an enum inside the .xcdatamodeld file itself. But as said, by creating and designing the Order entity, Core Data created a corresponding class under the hood. We can access and modify this class by clicking on the Order entity, going to the Xcode toolbar, and selecting Editor-“Create NSObjectManagedSubclass”. 

After creating the subclass, Xcode generated two files for us. The Order+CoreDataClass.swift file holds the class itself, and the Order+CoreDataProperties.swift contains its properties inside an extension. 

After we created our data model’s subclass, we need to tell Xcode that the data model is no longer defined by the visual builder in our “.xcdatamodeld” file only, but manually defined by the corresponding subclass we just created. To do this, open the “.xcdatamodeld” file, click on the Order entity and open the data model inspector. Then choose “Manual/None” as the Codegen mode.

At this point, we can remove the question marks from the String-type properties since we don’t want them to be Optionals. Xcode should also create another extension adopting the Identifiable protocol (this will make it easier for us to use Order instances inside the ContentView’s List later). Since we declared an id property, we already conform to this protocol.

extension Order: Identifiable {

    @nonobjc public class func fetchRequest() -> NSFetchRequest<Order> {
        return NSFetchRequest<Order>(entityName: "Order")
    }

    @NSManaged public var pizzaType: String
    @NSManaged public var numberOfSlices: Int16
    @NSManaged public var id: UUID?
    @NSManaged public var tableNumber: String
    @NSManaged public var status: String

}

extension Order : Identifiable {

}

Below the Order extension we can declare our Status enum with the three different cases.

enum Status: String {
    case pending = "Pending"
    case preparing = "Preparing"
    case completed = "Completed"
}

If we now try to use the Status enum as the status’ data type, we will get an error.

You see that @NSManagedObject properties can’t be used with enums directly. But how else can we save the status of an order in Core Data? Here’s a workaround: We go ahead with using our NSManaged status property but not of our Status type. Instead, it should be a String again. Next, we add another regular variable called “orderStatus”. Because it’s not an NSManaged property, it can be of the type Status. We assign a setter and getter to our orderStatus. When this property is set, it will also set the NSManaged property accordingly. Using a getter, we try to convert the status string to a Status case when retrieving it.

extension Order {

    //...
    @NSManaged public var status: String
    
    var orderStatus: Status {
        set {
            status = newValue.rawValue
        }
        get {
            Status(rawValue: status) ?? .pending
        }
    }

}

Awesome, we finalized the Core Data model for our SwiftUI app!

Composing our UI 🎨

Important: Before moving on with composing our ContentView, we need to make sure that its preview can access the view Context as well. Otherwise the SwiftUI preview will fail when why try to implement CoreData functionality inside it. To do this, we use the viewContext of our PersistenceController and assign it to the environment’s managedObjectContext key just as we did in our App struct.

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
    }
}

Now our ContentView preview is able to manage CoreData requests!

The ContentView of our pizza restaurant app should contain a list of all orders already taken which the corresponding waiter can manage. Since we can’t store any data yet, we are using only a test list for now.

struct ContentView: View {
    var body: some View {
        List {
            Text("Sample order")
        }
    }
}

We also want to add a navigation bar to our app. To do so, we wrap our List into a NavigationView and use the .navigationBarTitle modifier.

NavigationView {
    List {
        Text("Sample order")
    }
        .navigationTitle("My Orders")
}

The navigation bar should contain a button the waiter can use to add a new order.

List {
    Text("Sample order")
}
    .navigationTitle("My Orders")
    .navigationBarItems(trailing: Button(action: {
        print("Open order sheet")
    }, label: {
        Image(systemName: "plus.circle")
            .imageScale(.large)
    }))

The preview canvas should look like this so far:

When we tap on the Button, we want to open a second view. For this, we create a new SwiftUI file and name it “OrderSheet. We want to display the OrderSheet as a modal view. To do this, we add a State to our ContentView to control when the OrderSheet should be displayed.

struct ContentView: View {
@State var showOrderSheet = false
    var body: some View {
       //...
    }
}

To display the OrderSheet as a modal view, we use the .sheet modifier.

List {
    Text("Sample order")
}
    //...
    .sheet(isPresented: $showOrderSheet) {
        OrderSheet()
    }

Whenever the showOrderSheet State is true the OrderSheet overlays the ContentView. Now we can toggle the showOrderSheet State from our navigation bar button.

.navigationBarItems(trailing: Button(action: {
    showOrderSheet = true
}, label: {
    Image(systemName: "plus.circle")
        .imageScale(.large)
}))

For our OrderSheet view’s body, we’ll be using the Form view to embed the user controls in, for example, a Picker with the different pizza options available. To represent the number of slices that the customer wishes to order, we use a Stepper. Finally, we use a TextField where the user can select the table number for the order.

Finally, we want to save the data after the user taps on the “Add Order” button.

For the OrderSheet’s UI, you can use copy & paste the following code:

struct OrderSheet: View {
       
    let pizzaTypes = ["Pizza Margherita", "Greek Pizza", "Pizza Supreme", "Pizza California", "New York Pizza"]
    
    @State var selectedPizzaIndex = 1
    @State var numberOfSlices = 1
    @State var tableNumber = ""
    
    var body: some View {
        NavigationView {
            Form {
                Section(header: Text("Pizza Details")) {
                    Picker(selection: $selectedPizzaIndex, label: Text("Pizza Type")) {
                        ForEach(0 ..< pizzaTypes.count) {
                                Text(self.pizzaTypes[$0]).tag($0)
                        }
                    }
                    
                    Stepper("\(numberOfSlices) Slices", value: $numberOfSlices, in: 1...12)
                }
                
                Section(header: Text("Table")) {
                    TextField("Table Number", text: $tableNumber)
                        .keyboardType(.numberPad)
                    
                }
                
                Button(action: {
                    print("Save the order!")
                }) {
                    Text("Add Order")
                }
            }
                .navigationTitle("Add Order")
        }
    }
}

The OrderSheet’s preview should now look like this:

Saving data using Core Data and SwiftUI 🆕

Great, we’re done composing our PizzaRestaurant app’s interface, but nothing gets saved and persisted yet. To change this, we need to access to the viewContext first to persistently save a created order. Since, as we saw in the beginning, the managed object context is injected in our environment, we can simply access it by using the @Environment property wrapper inside our OrderSheet above its States.

@Environment(\.managedObjectContext) private var viewContext

Now that our OrderSheet has access to the device’s “scratchpad” we are ready to create an Order instance when tapping on the “Add Order” Button. But first, we want to make sure that the tableNumber String is not empty by using a guard statement.

Button(action: {
    guard self.tableNumber != "" else {return}
    let newOrder = Order(context: viewContext)
    newOrder.pizzaType = self.pizzaTypes[self.selectedPizzaIndex]
    newOrder.orderStatus = .pending
    newOrder.tableNumber = self.tableNumber
    newOrder.numberOfSlices = Int16(self.numberOfSlices)
    newOrder.id = UUID()
}) {
    Text("Add Order")
}

Then we’re trying to save the created order. If that fails, we print the corresponding error.

Button(action: {
    //...
    newOrder.id = UUID()
    do {
        try viewContext.save()
        print("Order saved.")
    } catch {
        print(error.localizedDescription)
    }
}) {
    Text("Add Order")
}

After the new order got saved, we want to close the OrderSheet modal view. We can do this by adding the following @Environment property to our OrderSheet.

@Environment (\.presentationMode) var presentationMode

By referring to this property we can manually close the modal view:

do {
    try viewContext.save()
    print("Order saved.")
    presentationMode.wrappedValue.dismiss()
} catch {
    print(error.localizedDescription)
}

Okay, let’s run our app to see if that works. Note that the preview canvas isn’t able to simulate CoreData’s functionality. Therefore, we need to run the app in the regular simulator. Click on the navigation bar button and fill out the OrderSheet form. Then click on “Add Order”. We saved the created order and dismissed the OrderSheet. However, our ContentView’s List is still displaying its sample row.

Fetching and displaying stored orders 📖

To change this, our ContentView needs to read out the saved orders. Achieving this functionality is quite simple by using a @FetchRequest property. But first, our ContentView itself requires access to the viewContext. We do this by using the @Environment property again. Below the ContentView’s @Environment property, insert the following properties:

@Environment(\.managedObjectContext) private var viewContext

@FetchRequest(entity: Order.entity(), sortDescriptors: [], predicate: NSPredicate(format: "status != %@", Status.completed.rawValue))

var orders: FetchedResults<Order>

The @FetchRequest permanently reads out the persistent storage for fetching stored orders from it. With the “predicate” argument, we filter out all orders already completed since we don’t want them to display in our ContentView’s List. The @FetchRequest then passes the retrieved orders to the orders property. Whenever we save a new order, the @FetchRequest will notice and add it to the orders data set. Similar to the State functionality, this causes the ContentView to renew its body.

Now we’re ready to display the fetched data inside our List, like this:

List {
    ForEach(orders) { order in
        HStack {
            VStack(alignment: .leading) {
                Text("\(order.pizzaType) - \(order.numberOfSlices) slices")
                    .font(.headline)
                Text("Table \(order.tableNumber)")
                    .font(.subheadline)
            }
            Spacer()
            Button(action: {print("Update order")}) {
                Text(order.orderStatus == .pending ? "Prepare" : "Complete")
                    .foregroundColor(.blue)
            }
        }
        .frame(height: 50)
    }
}
    .listStyle(PlainListStyle())
    //...

Hint: The reason we use a ForEach loop inside the List instead of inserting the orders data set in the List itself will become clear when deleting orders.

When we run our app again, we see that our @FetchRequest successfully retrieves the just saved order from the persistent storage.

Hint: Also, our ContentView preview shows us different orders. However, these are not the ones that are located in the device’s persistent storage, e.g., the simulator. Rather, they are generated for test purposes by the preview property of our PersistenceController. Can you remember when we adapted the corresponding code at the beginning? The resulting sample Order instances are now used by the preview simulator, which itself has no persistent storage.

Updating Core Data entries 🔄

The Button on the right side of each row can be used to update the particular Order’s status. When we add a new Order, its status is .pending. Therefore the Button reads “Prepare”. When the user taps on the Button we want to update the status to .preparing, and the Button should read “Complete”. When the user taps again, we want the Order’s status to be .completed, which causes the @FetchRequest to filter the Order out.

To implement this functionality, we add the following function below our ContentView’s body.

func updateOrder(order: Order) {
        let newStatus = order.orderStatus == .pending ? Status.preparing : .completed
        viewContext.performAndWait {
            order.orderStatus = newStatus
            try? viewContext.save()
        }
    }

We can call the updateOrder function from our row’s button with passing the particular order instance:

Button(action: {
    updateOrder(order: order)
}) {
    Text(order.orderStatus == .pending ? "Prepare" : "Complete")
        .foregroundColor(.blue)
}

Now we can run the app and tap on the “Prepare” button to mark the currently pending order as prepared. If we click on “Complete”, the Order will be filtered out and eventually removed from our List.

Deleting orders from the persistent storage 🗑

Deleting stored data is almost as simple as updating it. All we have to do is to delete the specific Order from the viewContext. Then, since the @FetchRequest will automatically detect that the Order was deleted, it will update our ContentView accordingly and remove the row from the table with a nice default animation.

To let the user delete rows, we add the .onDelete modifier to the ForEach loop. We can’t apply this modifier to Lists. This is why we inserted a ForEach loop inside the List.

List {
    ForEach(orders) { order in
        //...
    }
        .onDelete { indexSet in
            for index in indexSet {
                viewContext.delete(orders[index])
            }
            do {
                try viewContext.save()
            } catch {
                print(error.localizedDescription)
            }
        }
}

The .onDelete modifier detects the row(s) the user wants to delete by swiping and uses there index/indices to remove the corresponding Order entries from the viewContext.

If we run the application now, we can see that we can easily delete the order by swiping a row.

Conclusion 🎊

That’s it. We finished our small pizza restaurant app! You learned how to use Core Data in SwiftUI to store data persistently. We talked through all basic CRUD operations: Creating, reading, updating, and deleting data. We also understood what a managedObjectContext is and how we can fetch stored data by using SwiftUI’s @FetchRequest.

We’ve uploaded the whole source code of this app to GitHub.


If you liked this tutorial, feel free to check out our Mastering SwiftUI eBook. In this book, we also created a To-do app by using the mentioned Core Data functionalities!

I hope you enjoyed this tutorial! If you want to learn more about SwiftUI, check out our other tutorials! Also, make sure you follow us on Instagram and subscribe to our newsletter to not miss any updates, tutorials, and tips about SwiftUI and more!

26 replies on “Core Data and SwiftUI 2.0 – Saving, retrieving, updating and deleting persistent data”

While this tutorial did not cover what I was looking for, namely cloudKit integration with CoreData, I think it’s an awesome short to the point tutorial! I wish I had it when I first started using CoreData with SwiftUI. Thanks for delivering this to all the people who will benefit!

Hi,
I followed the same pattern. Somehow the data gets saved in the Persistent container. But it’s not displaying. Seems to be an issue with FetchRequest. No errors though!

thanks – reviewing now. Actually the Content View “preview” didn’t work for me, got an error to direct me to crash reports. Does it work for you? (I’m on Xcode 11.4)

Hi Greg, thanks for your comment. We just added the necessary struct right below the “Composing our UI 🎨” headline

Thanks a lot, this tutorial is really useful, only a little that your write more, because the “ContentView_Previews” can not display the right content, so you need to add the code in this struct:

let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
return ContentView().environment(\.managedObjectContext, context)

Thanks for a great tutorial!

Only noticed a minor bug: when adding an order, you cannot immediately add another (only after e.g. changing the orderStatus of the first)

Thanks for a great tutorial! Your descriptions and thoroughness were outstanding.

Where can I find out more about multiple adds/inserts at the same time? For example, what if the same table wanted more than one type of pizza…2 slices greek, 1 slice NY, 1 slice CA?

This is a really great tutorial. Nice and to the point while introducing a design pattern that is actually usable and extendable in real-life apps.
Thanks for sharing!

Following the code, deleting a order, stopping the app than restarting the app the order is still there.

Hey Bruce,

I could not reproduce this behaviour. Could you send me your Project and I’ll take a look at it?

Thank you!

Hi Bruce,

Just a guess but check you’ve attached the .onDelete modifier to the end of the ForEach code block. I’ve had it before where the code is on the wrong block and you get strange behaviour.

just try to save the managedObjectContext after deleting an order

self.managedObjectContext.delete(self.orders[index])
try? self.managedObjectContext.save()

Thanks for this awesome tutorial.
In the updateOrder func, should we must use the performAndWait method?

A very good tutorial. I‘m looking for an example using a view for updating the records. How can I change the attributes of a Data object and then put the changed record to the persistent store?

I was following the tutorial. At a point where we would display all the entries, I got an error saying: “The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions”. Thought if I just follow till the end it would fix itself somehow. It didn’t.

After literally copy-pasting code from your repo, it still has this error.
However, if I clone your repo and run your project it works.
Any ideas?

Env:
Xcode Version 11.5 (11E608c)
macOS Catalina 10.15.5 (19F101)

When you create, update or delete entities, you do so in your managed object context — the in-memory scratchpad. To actually write the changes to disk, you must save the context. This method saves new or updated objects to the persistent store.

This is an excellent tutorial. My only criticism is that it doesn’t really cover the “Read” part of CRUD. It would be nice to see how to edit an order. I understand that tapping on it updates the order status (this was really helpful to understand) but I wish we could have learned how to load the selected object back into the OrderSheet. Other than that, awesome. Thank you.

Why the entire HStack of the row of order become a button? it button area should really just be the word “preparing” or “complete” right?

Leave a Reply

Your email address will not be published. Required fields are marked *

small_c_popup.png

Covid-19 Forces you into quarantine?

Start Mastering swiftUI Today save your 33% discount