Fetching and saving To-Do items

Fetching ToDoItems ⬇️

Okay, now it’s time to retrieve our To-Do’s from the device’s internal storage. As already learned, fetching, saving, etc. happens in the viewContext. We can access it by using the @Environment wrapper above our newTaskTitle State.

@Environment(\.managedObjectContext) var viewContext

Now we are able to execute CoreData fetch requests for retrieving stored To-Do items. Doing this in SwiftUI is surprisingly simple using the @FetchRequest property wrapper.

This property wrapper looks inside the internal storage by using the viewContext and fetches the specified entity. Insert the following property below the @Environment property. We will fix the resulting error in a second.

@FetchRequest(entity: ToDoItem.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \ToDoItem.createdAt, ascending: false)], predicate: NSPredicate(format: "taskDone = %d", false), animation: .default)

Let’s go through this code step-by-step!

  • The @FetchRequest needs to know which entity to use. In our case, that’s the created ToDoItem entity.
  • Then we have to determine how CoreData should sort the fetched objects. We want to sort every item by the date and time it was created. We do this by using an NSSortDescriptor and specifying the attribute we want to use for sorting the fetched results.
  • We also filter the fetched results by using the “predicate” argument. In our case, we only want to show items that are not marked as done.
  • With the “animation” argument we can define if and how new added or removed ToDoItem instances should be animated

We can now pass the fetched data to another variable for holding the To-Do items. This will also fix our error.

var fetchedItems: FetchedResults<ToDoItem>

Make sure you place this variable below the @FetchRequest property.

Great! Every time some item in our storage gets deleted or added, our @FetchRequest property wrapper fetches all items and causes the ContentView to rebuild its body.

Let’s add one last variable to retrieve the ToDoItem instances from the fetchResults:

var fetchResults: [ToDoItem] {
    var results = [ToDoItem]()
        for item in fetchedItems {
            results.append(item)
        }
    return results
}

Updating our view’s body 🔄

We can use our fetchResults instead of our sampleTasks to feed the ForEach loop in our ContentView.

Therefore, delete the sampleTask array, and use the fetchResults property instead.

ForEach(fetchResults, id: \.self) { item in
    HStack {
        Text(item.taskTitle ?? "Empty")
        //...
    }
}

Note: To enable our Preview simulator to access the CoreData functionality and especially to use the @FetchRequest property, we have to provide it with a viewContext as well. For this purpose, we use the .environment modifier again as we already did in our ToDo_App struct.

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

The ForEach loop cycles through the ToDoItems in the fetchResults, but none exists because we haven’t created any tasks yet. Therefore, we only see our two static rows and the dummy “Preview Tasks” generated by the preview property in our PersistenceController struct.

Next, we need to add an actual ToDoItem to our database when the user enters a new task and taps on the “Plus”-Button.

Saving new items 🆕

To save a new To-Do item, we create a function named “save”. We call save when the user either clicks on the return key or when on the plus button after entering something in the TextField. Let’s place this function below our TaskView’s body.

func saveTask() {
    
}

We need to ensure that we only save a new task when the TextField isn’t empty. To do this, we write:

func saveTask() {
    guard self.newTaskTitle != "" else {
        return
    }
}

By doing this, we prevent the user from adding a task with an empty title.

Next, we are going to create a ToDoItem.

func saveTask() {
    //...
    
    let newToDoItem = ToDoItem(context: viewContext)
}

The values of this ToDoItem’s attributes are not determined yet.
Let’s change this:

func saveTask() {
    //...
    
    newToDoItem.taskTitle = newTaskTitle
    newToDoItem.createdAt = Date()
    newToDoItem.taskDone = false
}

Next, we try to save our newToDoItem. In case something goes wrong, we want to print out the specific error.

func saveTask() {
    //...
    
    do {
        try viewContext.save()
    } catch {
        print(error.localizedDescription)
    }
}

Finally, we reset our newTaskTitle State so the user can add another task. That’s all! Here’s the full function:

func saveTask() {
    guard self.newTaskTitle != "" else {
        return
    }
    
    let newToDoItem = ToDoItem(context: viewContext)
    
    newToDoItem.taskTitle = newTaskTitle
    newToDoItem.createdAt = Date()
    newToDoItem.taskDone = false
    
    do {
        try viewContext.save()
    } catch {
        print(error.localizedDescription)
    }
    
    newTaskTitle = ""
}

Now, we can use this function for the .onSubmit modifier of our TextField and the “Plus”-Button’s action:

HStack {
    TextField("Add task...", text: $newTaskTitle)
        .onSubmit {
            print("New task title entered.")
        }
    Button(action: {
        saveTask()
    }) {
        Image(systemName: "plus")
            .imageScale(.large)
    }
}

Great, let’s see if that works by running our app in the standard simulator.

Enter something in the TextField. Then, either click on the return key of the toggled keyboard or click on the plus button. Awesome! The saveTask function gets called, which reads the TextField’s State and uses the String assigned to it to create a new ToDoItem. The newToDoItem then gets saved to our database. The @FetchRequest again fetches all objects from the ToDoItem entity and passes them to the fetchedItems property resulting in an updated fetchResult.



Note: You can also do this in the preview simulator using a Live preview. But note that when stopping the Live preview, the injected viewContext gets reset, which causes the entered task to disappear again. Also note that the “Preview Tasks” provided by PersistenceController are displayed!



Now, we’ll need to find a way to mark a To-Do as done. This means that we have to find the corresponding ToDoItem in the database and set its taskDone attribute to false.

One reply on “Fetching and saving To-Do items”

Hallo Andreas; i repeat this tutorial working with OS Monterey and XCode 13.1: when i provide the .environment modifier to TaskView Preview i got a crash of simulator (Thread 1: Fatal error: UnsafeRawBufferPointer with negative count) and also crashed Xcode

Leave a Reply

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