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

Share on facebook
Share on twitter
Share on pinterest

Hello and welcome to a new tutorial! Today we will learn how to use the Core Data framework 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’re going to 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 in SwiftUI 🛠

To get started, open Xcode 11 and create a new Single view app and choose SwiftUI as the User Interface mode. Make sure that you check the “Use Core Data” box. This will automatically set up the initial Core Data implementation for our app!

When your project is created, you see that Xcode automatically generated a .xcdatamodeld file for us. Inside this file, we can visually design our model for managing persistent data as you’ll see in a moment.

Xcode also generated some interesting lines of code for us. Click on the SceneDelegate.swift file and take a look at the scene function inside the SceneDelegate class. The scene function handles booting up our initial view, which is the ContentView by default. Because we check marked “Use Core Data” when creating our SwiftUI project, Xcode applied some modifications to this function.

Take a look at this line of code within the scene function:

let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext

This function retrieves the managed object context for us. A managed object context is like an in-memory scratchpad. It’s the place where objects are created, fetched, updated, deleted, and saved back to the persistent store of the device where the app runs on.

Let’s jump to the next line of code:

let contentView = ContentView().environment(\.managedObjectContext, context)

This line initializes the ContentView which is then set as the root view in the lines below. So far, so good. But what does this environment thing do?

Before our ContentView gets launched as the root view, it feeds the environment’s managedObjectContext key with our context we just created. The environment is the place where system-wide settings are stored in, e.g Calendar, Locale, ColorScheme and now also our created context (our app’s scratchpad). Each of this setting has its own key, in our case, it’s the .managedObjectContext key.

Now every view in our app can use the “context scratchpad” to retrieve, update and store objects from. We just have 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 now for fetching and saving our orders and you’ll see how easy it is in a moment.

Defining our data model 🛠

As said, in the project navigator, you can see a .xcdatamodeld file. In this file, we create and manage the entities of our CoreData data model for our SwiftUI app. In case you are not familiar with core data entities: You can think of an entity as a representation of 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. Create the entity by clicking on the large plus sign at the bottom and then double-click on the created Entity and 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 an 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.

But let’s hold on a second and reconsider choosing String as the status attribute’s type. The 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. We can also adopt the Identifiable protocol (this will make it easier for us the use Order instances inside the ContentView’s List) and since we have an id property we already conform to this.

extension Order: Identifiable {

    //...
    @NSManaged public var pizzaType: String
    @NSManaged public var status: String
    @NSManaged public var tableNumber: String

}

Below the OrderExtension 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 creating an NSManaged status property but not of our Status type. Instead it should be a String. Next, we add another, regular variable called orderStatus. Because it’s not a NSManaged property, it can be of the type Status. We assign a setter and getter to our orderStatus, so that when this property is set, it will also set the NSManaged property accordingly and by using a getter, we try to convert the status string to an Status case when retrieving it.

extension Order: Identifiable {

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

}

Awesome, we’re now finished with creating 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 managedObjectContext as well. Otherwise the SwiftUI preview will fail when why try to implement CoreData functionality inside it. To do this, we repeat the steps as we did in the SceneDelegate.swift file in the beginning:

struct ContentView_Previews: PreviewProvider {
    
    static var previews: some View {
        
        let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
        
        return ContentView().environment(\.managedObjectContext, context)
    }
}

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")
            }
                .navigationBarTitle("My Orders")
        }

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

List {
     Text("Sample order")
            }
                .navigationBarTitle("My Orders")
                .navigationBarItems(trailing: Button(action: {
     print("Open order sheet")
                }, label: {
    Image(systemName: "plus.circle")
                        .resizable()
                        .frame(width: 32, height: 32, alignment: .center)
                }))

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: {
                    self.showOrderSheet = true
                }, label: {
                    Image(systemName: "plus.circle")
                        .resizable()
                        .frame(width: 32, height: 32, alignment: .center)
                }))


For our OrderSheet view’s body, we’ll be using the Form view in SwiftUI and a picker with the different pizza options. The number of slices that the customer wishes to order will be represented by a stepper. Finally, we use a text field where the user can write the table number for the order.

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")
                }.navigationBarTitle("Add Order")
                
            }
        }
    }
}

The OrderSheet’s preview should now look like this:

Saving orders 🆕

Great, we’re done with our app’s UI, but nothing gets saved and persisted yet. To change this we need to access to the managed object context 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) var managedObjectContext

Bug-Alert 🐞: Unfortunately there’s a bug in the current version of SwiftUI. Since we injected into the SceneDelegate’s scene function, the managed object context should be accessible globally by all views by using the corresponding environment property. However, doing this inside popover views like our OrderSheet won’t work properly. What we have to do is to pass the managedObjectContext that gets initialised inside the scene function downwards to our OrderSheet. Thus, we have to use the @Environment property inside our ContentView, too …

@Environment(\.managedObjectContext) var managedObjectContext
    
@State var showOrderSheet = false
//…

… and pass it to the OrderSheet inside the .sheet modifier:

.sheet(isPresented: $showOrderSheet) {
    OrderSheet().environment(\.managedObjectContext, self.managedObjectContext)
                }

Now that our OrderSheet has access to the device’s “scratchpad” we are ready to create an Order instance inside our “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: self.managedObjectContext)
                    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.

//...
newOrder.id = UUID()
do {
  try self.managedObjectContext.save()
  print("Order saved.")
 } catch {
  print(error.localizedDescription)
  }

After the new order got saved, we want to close the OrderSheet modal view. We can do this by inserting the following environment property into our OrderSheet.

@Environment (\.presentationMode) var presentationMode

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

do {
try self.managedObjectContext.save()
print("Order saved.")
self.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 Core Data’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 dismisses 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. Below the ContentView’s environment property insert the following lines of code:

@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 rebuild itself.

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)
                        }
                    }
                }
            }

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

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

Updating orders 🔄

The button on each row’s right side can be used to update the status of the particular order. 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
        managedObjectContext.performAndWait {
            order.orderStatus = newStatus
try? managedObjectContext.save()
        }
    }

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

Button(action: {self.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 then 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 do is to delete the particular object from the managed object context, and then, since the @FetchRequest will automatically detect that the object 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 row. We can’t apply this modifier to Lists, this is why we use a ForEach loop inside the List.

ForEach(orders) { order in
                    //...
                }
                    .onDelete { indexSet in
                        for index in indexSet {
                            self.managedObjectContext.delete(self.orders[index])
                        }
                    }

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 managedObjectContext.

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’re finished with our small pizza restaurant app! We learned how we can 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.

mIf 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!

6 replies on “Core Data and SwiftUI – 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!

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)

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

small_c_popup.png

Are you ready for a new era of iOS development?

Start learning swiftUI today - download our free e-book