How To Make a Task List Using SwiftUI and Core Data

Build a task-list app that stores tasks in Core Data

Maegan Wilson
Dec 18, 2019 · 8 min read
Photo by Emma Matthews Digital Content Production on Unsplash

This app will have the following features:

  • Add a task
  • Complete a task
  • Delete a task
  • Persist data if the app is closed

Here’s what the finished app will look like:

The finished app can be found on this GitHub repo.


1. Create a new single-page iOS app

Create a new Xcode project for a single-view iOS app.

Check the box for SwiftUI and the box to use Core Data.


2. Core Data Entities and Attributes

The first thing we need to do is add an entity to the Core Data model. To do this, open ProjectName.xcdatamodeld, where ProjectName is what you called the project in Step 1, and click on Add Entity at the bottom of the window. Name the new entity Task.

The image below highlights where to change the name in the inspector.

2.1 Adding attributes to the task entity

Next, we need the Task entity to have attributes to store the following information:

  • id: used as a unique identifier for each task
  • name: what the user will call the task
  • isComplete: defines whether or not a task’s completed
  • dateAdded: to know when a task was added

To add attributes to Task, click the + in the Attributes section, and give the attribute a name and type. The GIF below shows how to do this.

This table describes each attribute and the type associated with the attribute.

  • id: UUID
  • name: string
  • isComplete: Bool
  • dateAdded: date

The ProjectName.xcdatamodeld should now look like the picture below.

2.2 Add a new Swift file

Now, we’re adding a new Swift file that’ll make Task identifiable, making the list of tasks easier to call.

Add a new Swift file, and call it Task+Extensions.

In the file, add the following:

extension Task: Identifiable {
}

By adding the code above, the Task class now conforms to the Identifiableclass.

2.3 Add Core Data to ContentView.swift

We need to add a variable that accesses our managedObjectContext in our ContentView.swift file.

To do this, open the ContentView.swift file, and add @Environment(.\managedObjectContext) var context before the bodyvariable. ContentView should now look like this:

struct ContentView: View {
@Environment(\.managedObjectContext) var context
var body: some View {
Text("Hello world!")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

What did we do?

We declared context as an environment variable, meaning the value is going to come from the view’s environment. In this case, it’s going to come from SceneDelegate.swift on lines 23 through 27, where context is declared and then given to ContentView().


3. Time to Make the UI Work

We’re now going to work on the UI in ContentView.swift.

3.1 Adding a TextField

Let’s start by adding a TextField to the app.

Change Text(HelloWorld) to TextField(title: StringProtocol, text:Binding<String>).

TextFieldneeds two properties: a StringProtocol and a Binding<String>. For the StringProtocol, give it a property of "Task Name". When the TextField is empty, Task Name will appear in light gray.

Now, we still need a Binding<String>. This isn't as easy as TextField. We need to declare a variable for it.

Before the body variable declaration, add @State private var taskName: String = "", and then make the second property of TextField $taskName.

ContentView.swift should now look like this:

struct ContentView: View {
@Environment(\.managedObjectContext) var context
// this is the variable we added
@State private var taskName: String = ""
var body: some View {
// this is the TextField that we added
TextField("Task Name", text: $taskName)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Since we are using SwiftUI, if you use the canvas, you can see what the UI looks like without having to run the app in the simulator.

What did we do?

I’m going to explain the parts of @State private var taskName: String = "" and why we needed to do this.

First, this is declaring a state property by using the @State property wrapper so taskName is a binding value. A state property is going to store the value in taskName and allow the view to watch and update when the value changes.

3.2 Adding the task to Core Data

First, we need to add a button so when the user is done typing, they can then add the task to their list.

To do this, we’re going to wrap TextField in an HStack and then add a Button(). When adding the button, the action should be self.addTask(), and the label in the button should be Text("Add Task).

Here’s what the code in the body should look like now.

var body: some View {
HStack{
TextField("Task Name", text: $taskName)
Button(action: {
self.addTask()
}){
Text("Add Task")
}
}
}

Now, this causes Xcode to give the error “Value of type 'ContentView' has no member 'addTask',” so this means we have to add the function addTask().

After the body variable, add the following:

func addTask() {
let newTask = Task(context: context)
newTask.id = UUID()
newTask.isComplete = false
newTask.name = taskName
newTask.dateAdded = Date()
do {
try context.save()
} catch {
print(error)
}
}

What did we do?

In addTask(), we made a new Task object and then gave the newTask values their respective attributes. Then, we use the save() on context to add it to Core Data.

Here’s what the UI looks like so far.

3.3 Creating the task list

It’s finally time to create the task list.

First, we need to make a fetch request to get the tasks added. Here’s what we need to add to the ContentView.

@FetchRequest(
entity: Task.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \Task.dateAdded, ascending: false)],
predicate: NSPredicate(format: "isComplete == %@", NSNumber(value: false))
) var notCompletedTasks: FetchedResults<Task>

Now, I’m going to break this down a bit.

  • entity declares what Core Data entity we’re retrieving
  • sortDescriptors describes how we want to sort the entities
  • predicate acts as a filter

So with the code above, we’re asking for all tasks that aren’t completed and for those tasks to be sorted by date — newest to oldest.

Next, we need to make a list that shows the tasks. Let’s embed the HStack inside a VStack. It should look like this:

VStack {
HStack {
// TEXTFIELD CODE HERE
}
}

Now, we can add a list. After the HStack, add the following:

List {
Text("Hi")
}

This adds a list underneath the TextField and makes the UI look like this.

Next, we’re going to make “Hi” repeat for however many tasks we have. Embed Text("Hi") inside a ForEach like this:

ForEach(notCompletedTasks){ task in
Text("Hi")
}

We didn’t have to specify the id for notCompletedTasks in the ForEach because Task conforms to Identifiable thanks to our work in Step 2.3.

If you run the app, then put in a task name. Hitting Add Task will make another row of “Hi.”

Let’s make a new struct for a TaskRow view that’ll take in the task in ContentView.swift. Above ContentView(), add the following:

struct TaskRow: View {
var task: Task
var body: some View {
Text(task.name ?? "No name given")
}
}

Inside the Text, you’ll see we have to use the nil-coalescing operator, ??, to give a default value. The reason we do this is because the value for the Task attributes are optional and might not have a value.

Now, inside the ForEach, replace Text with TaskRow(task). ContentView.swift should have the following code.

import SwiftUIstruct TaskRow: View {
var task: Task
var body: some View {
Text(task.name ?? "No name given")
}
}
struct ContentView: View {
@Environment(\.managedObjectContext) var context
@FetchRequest(
entity: Task.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \Task.dateAdded, ascending: false)],
predicate: NSPredicate(format: "isComplete == %@", NSNumber(value: false))
) var notCompletedTasks: FetchedResults<Task>
@State private var taskName: String = "" var body: some View {
VStack {
HStack{
TextField("Task Name", text: $taskName)
Button(action: {
self.addTask()
}){
Text("Add Task")
}
}
List {
ForEach(notCompletedTasks){ task in
TaskRow(task: task)
}
}
}
}
func addTask() {
let newTask = Task(context: context)
newTask.id = UUID()
newTask.isComplete = false
newTask.name = taskName
newTask.dateAdded = Date()
do {
try context.save()
} catch {
print(error)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Here’s how the app should work now.


4. Marking a Task As Complete

Now, we’re going to mark the task as complete, which should make the task disappear from the list.

First, we’re going to embed the TaskRow into a Button, and the action of the button is going to be self.updateTask(task). Now that’ll look like this.

Button(action: {
self.updateTask(task)
}){
TaskRow(task: task)
}

Next, we need to make a function called updateTask so we can actually update the task and mark it as complete.

After addTask, let's add func updateTask(_ task: task){}. Using the _ says we can ignore the argument label when calling the function. If you want to read more about argument labels, click here to read my post about it. Next, let's add the internals of the function.

let isComplete = true
let taskID = task.id! as NSUUID
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "Task")
fetchRequest.predicate = NSPredicate(format: "id == %@", taskID as CVarArg)
fetchRequest.fetchLimit = 1
do {
let test = try context.fetch(fetchRequest)
let taskUpdate = test[0] as! NSManagedObject
taskUpdate.setValue(isComplete, forKey: "isComplete")
} catch {
print(error)
}

Let’s dive into this a bit. The first thing we do is set a constant for the new value of isComplete for the task. Then, we set the id of the task to a constant to use in the predicate. Next, we need to create a fetch request that gets the specific task we’re updating. Then, we perform the update.

Now, if you run the app, the app will allow you to add a task and then tap on it to mark it as complete. Since we’re only using the noncompleted tasks in the list, the completed task disappears from the list.

The GIF below shows the final app.

Better Programming

Advice for programmers.

Maegan Wilson

Written by

I am an independent iOS developer and Apple product enthusiast.

Better Programming

Advice for programmers.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade