This post is going to walk through how to make a task list app that stores the task in Core Data.
This app will have the following features:
- Add a task
- Complete a task
- Delete a task
- Persist data if the app is closed
Here is what the finished app will look like
The finished app can be found in this GitHub repo
heyjaywilson / taskList
CoreData and SwiftUI task list
This post is going to walk through how to make a task list app that stores the task in Core Data.
This app will have the following features:
- Add a task
- Complete a task
- Delete a task
- Persist data if the app is closed
Here is what the finished app will look like
The finished app can be found in this GitHub repo:
1. Create a new single page iOS app
Create a new Xcode project for a single view iOS app.
Check the boxes for SwiftUI and to use Core Data.
2. CoreData Entities and Attributes
The first thing we need to do is add an entity to the CoreData 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…
1. Create a new single page iOS app
Create a new Xcode project for a single view iOS app.
Check the boxes for SwiftUI and to use Core Data.
2. CoreData Entities and Attributes
The first thing we need to do is add an entity to the CoreData 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 is 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.
Attribute | Type |
---|---|
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 are adding a new swift file that will 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 Identifiable
class.
2.3 Add CoreData to ContentView.swift
We need to add a variable that accesses our Managed Object Context in ContentView.swift
. To do this, open ContentView.swift
and add @Environment(.\managedObjectContext) var context
before the body
variable. 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 that the value is going to come from the view's environment. In this case, it is going to come from SceneDelegate.swift
on lines 23 through 27 where context
is declared and then given to ContentView()
.
3. UI Time and make it work!
We are 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 the Text(HelloWorld)
to TextField(title: StringProtocol, text:Binding<String>)
. TextField
needs 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 that 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 our CoreData
First, we need to add a button so that when the user is done typing, they can then add the task to their list.
To do this, we are going to wrap TextField
in an HStack
and then add a Button()
. When adding the button, the action should be self.addTask()
and label in the button should be Text("Add Task)
.
Here's what the code in 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 attributes of newTask
values. Then we use the save()
on context to add it to CoreData.
Here is 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 that are added. Here's what we need to add 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 are retrieving -
sortDescriptors
describes how we want to sort the entities -
predicate
acts as a filter
So with the code above, we are asking for all Tasks that are not 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 are 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 did not 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 and hitting Add Task will make another row of "Hi".
Let's make a new struct
for a TaskRow view that will 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 will see that 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 the Text
with TaskRow(task)
. ContentView.swift
should have the following code.
import SwiftUI
struct 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 is how the app should work now.
4. Marking a task as complete!
Now, we are going to mark the task as complete, which should make the task disappear from the list.
First, we are going to embed the TaskRow
into a Button
and the action of the button is going to be self.updateTask(task)
. Now that will look like this.
Button(action: {
self.updateTask(task)
}){
TaskRow(task: task)
}
Next, we need to make a function called updateTask
so that we can actually update the task and mark as complete.
After addTask
, let's add func updateTask(_ task: task){}
. Using the _
says that 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 that we are 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 as complete. Since we are only using the non-completed tasks in the list, the completed task disappears from the list. The gif below shows the final app.
Edit #1: Posted on 2019-01-09
Here's a finished project as an example if you wanted to implement a Core Data helper class for the functions addTask()
and updateTask()
.
If you enjoy my posts, please consider sharing it or Buying me a Coffee!
Top comments (17)
Your post has been very helpful to me. Thank you.
I would like to ask you if you can create a swift file that groups all the methods (addTask, updateTask, ect) that can then be used in each view.
In swift with uikit I had create CoredataHelper shared class for use in every app with little modify.
Thank you
You definitely can! I do this in my production apps. I generally then add the helper as an
EnvironmentObject
so that I can access it when I need it.You can see my messy source code for one of my apps here. The
HelperClasses
folder will have the CoreData helperCalculationManager.swift
. This updates and deletes items from my CoreData context.Thank you. It is very difficult, for me. Could you tell me how to extract the methods from your TaskList, since I don't think I can do it?
Here's a branch of the original project with what you're looking to do that's been implemented.
Create a new file with the functions like this one linked here.
In the
ContentView.struct
file, addlet core_data_helper = CoreDataHelper()
. Then in the button's actions, call the functions fromcore_data_helper
.Here's an example of
addTask()
:Here's a link to what
ContentView.swift
should look like.Great solution. But, wanting to move from ContentView to CoreDataHelper:
@Environment(.managedObjectContext) var context
and
@FetchRequest(
entity: Task.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \Task.dateAdded, ascending: false)],
predicate: NSPredicate(format: "isComplete == %@", NSNumber(value: false))
) var notCompletedTasks: FetchedResults
How can it be done?
It's possible?
Thank you.
You can, but the
@Environment
and@FetchRequest
are used with SwiftUI views. I wouldn't move the@Environment
to make sure that there is a context associated with the view. I also like to leave my FetchRequests in the view because that's where I need the data to be viewed.Your explanation convinced me. Thank you.
Whenever I add the notCompletedTasks to the forEach loop it says that my app has crashed. How could I fix this, thanks for any help. I tried with your code as well with the same crash error.
ForEach(notCompletedTasks) { task in
Text("hi")
}
Is your core data entity class extending
Identifiable
? If not, then you will need to specify the id property of the task.I have added the Identifiable extension without luck.
Hi Maegan! This is very helpful. I'm trying to do something similar but using a One-To-Many relationship with Core Data.
Thanks I'm glad you found it useful!
At some point, I'd like to have more posts about Core Data and SwiftUI.
Looking forward to them and I'll keep hoping stackoverflow can come to the rescue haha
I know this feeling all too well! Hacking with Swift is also a great guide!
Been using Paul's resources! It's my favorite but trying to make something is way different than following the guides haha. Appreciate your responsiveness!
Thanks for the helpful post Maegan!
It helped me, a SwiftUI beginner, a great deal. But when I tried to build up I definitely got stumped.
I have a need for similar functionality but with the need to update a text field such as "name" rather than the Boolean "isComplete". I changed your code to update the "name" field from a constant:
This did not update CoreData. But, If when I added the following after the .setValue, core data is updated.
However, the tab view is not updated until I close and re-open the app.
Any assistance that you can provide will be immensely helpful!
I’ve run into this issue myself. I solved this by using an environment object that stores an array of the objects. It’s probably a bit of a performance hit, but it was a work around. I do mean to look into it at some point though. I’m sorry that I couldn’t be more help.