SwiftUI Core Data Tutorial: Integrating Core Data and Building a Todo List App (Save, Delete, Fetch & Update)

Rizal Hilman
11 min readJul 24, 2023

--

SwiftUI x CoreData — Create, Read, Update, Delete.

Welcome to the SwiftUI Core Data Tutorial! In this tutorial, you will learn how to integrate Core Data into your existing SwiftUI project and build a Todo List app with features like saving, deleting, fetching, and updating todo items.

Before we begin, make sure you have cloned or downloaded the starter project from the GitHub repository “drawrs/SwiftUI-Simple-TodoList” at the branch “crud_coredata_starter.” or open link below:

Let’s get started by adding Core Data to our project

Integrating Core Data to existing SwiftUI Project

When creating a SwiftUI project, you have the option to include Core Data or not. If you checked it, Xcode will automatically create the Core Data model, as you can see in the Project Navigator. However, if you missed checking it, you’ll need to add Core Data manually to your app project.

Creating Core Data Model

Go to File > New > Find “Data Model” under the Core Data section:

Name the Core Data model “Todolist.xcdatamodel”:

Adding a Database Entity

Now, you have the Todolist.xcdatamodeld file shown in the Project Navigator. Open that file and add a new entity, as shown in the following image:

Configure Entity Classes

Select the “Todo” entity that you just created, open the Inspector, and change the Codegen to Manual/none.

Create NSManagedObject class for our entities by going to Editor > Create NSManagedObject Subclass > Then select all entities.

Handling Conflict Model Name

If you encounter this error because your Todolist project already has a Todo struct, you need to rename it temporarily to TodoDummy:

Hit “Enter” to save the changes.

Preparing the Data Manager Class

We will use this class to represent our Core Data model. Create a new class named DataManager, and here is the code:

import CoreData
import Foundation

// Main data manager to handle the todo items
class DataManager: NSObject, ObservableObject {

// Add the Core Data container with the model name
let container: NSPersistentContainer = NSPersistentContainer(name: "Todolist")

// Default init method. Load the Core Data container
override init() {
super.init()
container.loadPersistentStores { _, _ in }
}
}

💡 Note: If you have a different data model name, change the part “NSPersistentContainer(name: “Todolist”)” to your own data model file name.

Update the SwiftUI_Simple_TodoListApp root view and add our Core Data environment:

struct SwiftUI_Simple_TodoListApp: App {
// MARK: Core data
@StateObject private var manager: DataManager = DataManager()

var body: some Scene {
WindowGroup {
ContentView()
// MARK: Core data
.environmentObject(manager)
.environment(\.managedObjectContext, manager.container.viewContext)
}
}
}

Saving in Core Data

Input Form View

We have created a view for inputting our new todo list so we can focus on handling Core Data operations. Inside the View group folder, please open TodoInputForm and define our Core Data variables (manager & viewContext):

import SwiftUI

struct TodoInputForm: View {
// MARK: Core data variables
@EnvironmentObject var manager: DataManager
@Environment(\.managedObjectContext) var viewContext

@Binding var isPresented: Bool
@State private var title: String = ""
@State private var date: Date = Date()
@State private var status: TodoStatus = .pending

var body: some View {
NavigationView {
Form {
Section {
TextField("Enter todo title", text: $title)
} header: {
Text("Title")
}

Section {
DatePicker("Select a date", selection: $date, displayedComponents: .date)
} header: {
Text("Date")
}

Section {
Picker("Select status", selection: $status) {
ForEach(TodoStatus.allCases, id: \.self) { status in
Text(status.rawValue)
}
}
.pickerStyle(SegmentedPickerStyle())
} header: {
Text("Status")
}
}
.navigationTitle("New Todo")
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {
isPresented = false
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button("Save") {
// Save the data and dismiss the sheet

// Call a function to handle saving or further processing of the newTodo
// For example, you can pass it to a delegate or callback.
isPresented = false
}
}
}
}
}

}

Save Function

We also have several state variables to holds our user input which are title, date and status. Now let’s create a new function to perform core data save operation:

// MARK: Core Data Operations
func saveTodo(title: String, date: Date, status: String) {
var todo = Todo(context: self.viewContext)
todo.id = UUID()
todo.title = title
todo.date = date
todo.status = status

do {
try self.viewContext.save()
print("Todo saved!")
} catch {
print("whoops \\(error.localizedDescription)")
}
}

Then call saveTodo function when user tap save button:

ToolbarItem(placement: .navigationBarTrailing) {
Button("Save") {
// Save the data and dismiss the sheet
self.saveTodo(title: title, date: date, status: status.rawValue)

// Call a function to handle saving or further processing of the newTodo
// For example, you can pass it to a delegate or callback.
isPresented = false
}
}

Our updated TodoInputForm view code would look like this, now try to run it to the simulator!:

import SwiftUI

struct TodoInputForm: View {
// MARK: Core data variables
@EnvironmentObject var manager: DataManager
@Environment(\.managedObjectContext) var viewContext

@Binding var isPresented: Bool
@State private var title: String = ""
@State private var date: Date = Date()
@State private var status: TodoStatus = .pending

var body: some View {
NavigationView {
Form {
Section {
TextField("Enter todo title", text: $title)
} header: {
Text("Title")
}

Section {
DatePicker("Select a date", selection: $date, displayedComponents: .date)
} header: {
Text("Date")
}

Section {

Picker("Select status", selection: $status) {
ForEach(TodoStatus.allCases, id: \.self) { status in
Text(status.rawValue)
}
}
.pickerStyle(SegmentedPickerStyle())
} header: {
Text("Status")
}

}
.navigationTitle("New Todo")
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {
isPresented = false
}
}

ToolbarItem(placement: .navigationBarTrailing) {
Button("Save") {
// Save the data and dismiss the sheet
self.saveTodo(title: title, date: date, status: status.rawValue)
// Call a function to handle saving or further processing of the newTodo
// For example, you can pass it to a delegate or callback.
isPresented = false
}
}
}
}
}

// MARK: Core Data Operations
func saveTodo(title: String, date: Date, status: String) {
let todo = Todo(context: self.viewContext)
todo.id = UUID()
todo.title = title
todo.date = date
todo.status = status

do {
try self.viewContext.save()
print("Todo saved!")
} catch {
print("whoops \(error.localizedDescription)")
}
}

}

Fetching / Displaying Todo List Data

Now we’ve successfully inserted a new todo list item, but the list you see is not from the database; it’s just a dummy list from an array. We need to fetch Todo data from Core Data. Open DataManager and add a new published todos variable:

/// Main data manager to handle the todo items
class DataManager: NSObject, ObservableObject {

/// Dynamic properties that the UI will react to
@Published var todos: [Todo] = [Todo]()

/// Add the Core Data container with the model name
let container: NSPersistentContainer = NSPersistentContainer(name: "Todolist")

/// Default init method. Load the Core Data container
override init() {
super.init()
container.loadPersistentStores { _, _ in }
}
}

Now, open ContentView and replace the results variable with a Core Data fetch request variable:

@FetchRequest(sortDescriptors: []) private var todos: FetchedResults<Todo>

Replace the results variable in the ForEach loop with the todos variable and handle optionality for title, date, and status:

ForEach(todos, id: \.self) { todo in
NavigationLink(destination: TodoDetailView(todo: todo)) {
HStack(alignment: .center) {
VStack(alignment: .leading) {
Text(todo.title ?? "")
.font(.title3)
Text(formatDate(todo.date ?? Date()))
.font(.subheadline)
.foregroundColor(.gray)
}
Spacer()
StatusIndicator(status: todo.status == "completed" ? .completed : .pending)
}
}
}

💡 You will get some error in TodoDetailView view, we will work on that later.

Since we replaced results variable our search feature won’t work. To make the search feature work again, observe the change of searchKeyword and use Core Data's NSPredicate for searching. Add a new .onChange modifier in the List view:

List {
// list code here
}
.searchable(text: $searchKeyword)
.onChange(of: searchKeyword) { newValue in
// MARK: Core Data Operations
self.todos.nsPredicate = newValue.isEmpty ? nil : NSPredicate(format: "title CONTAINS %@", newValue)
}

Now, the ContentView code will look like this:

import SwiftUI

struct ContentView: View {

// MARK: user typed keyword
@State var searchKeyword: String = ""
@State var isSheetPresented: Bool = false
// MARK: Core Data
@FetchRequest(sortDescriptors: []) private var todos: FetchedResults<Todo>

var body: some View {
NavigationView {
List {
ForEach(todos, id: \.self) { todo in
NavigationLink(destination: TodoDetailView(todo: todo)) {
HStack(alignment: .center) {
VStack(alignment: .leading) {
Text(todo.title ?? "")
.font(.title3)
Text(formatDate(todo.date ?? Date()))
.font(.subheadline)
.foregroundColor(.gray)
}
Spacer()
StatusIndicator(status: todo.status == "completed" ? .completed : .pending)
}
}
}
}
.listStyle(.inset)
.padding()
.navigationTitle("Todo List")
// MARK: Add searchable modifier
.searchable(text: $searchKeyword)
.onChange(of: searchKeyword) { newValue in
// MARK: Core Data Operations
self.todos.nsPredicate = newValue.isEmpty ? nil : NSPredicate(format: "title CONTAINS %@", newValue)
}
.sheet(isPresented: $isSheetPresented, content: {
TodoInputForm(isPresented: $isSheetPresented)
})

.toolbar {
Button("Add") {
isSheetPresented.toggle()
}
}
}
}

private func formatDate(_ date: Date) -> String {
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .short
return formatter.string(from: date)
}
}

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

To fix the error in TodoDetailView, replace TodoDummy with Todo, and handle optionality for title, date, and status:

struct TodoDetailView: View {
var todo: Todo

var body: some View {
VStack(alignment: .leading, spacing: 10) {
Text(todo.title ?? "")
.font(.title)
Text(formatDate(todo.date ?? Date()))
.font(.subheadline)
.foregroundColor(.gray)
StatusIndicator(status: todo.status == "completed" ? .completed : .pending)
}
.padding()
.navigationTitle("Todo Details")
}

private func formatDate(_ date: Date) -> String {
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .short
return formatter.string(from: date)
}
}

Awesome! Now run your app and you’ll see added Todo list item in Core Data.

Delete / Remove item from Core Data

Open ContentView add a new delete function:

private func delete(at offsets: IndexSet) {
for index in offsets {
let todo = todos[index]
// MARK: Core Data Operations
self.viewContext.delete(todo)

do {
try viewContext.save()
print("perform delete")
} catch {
// handle the Core Data error
}
}
}

Add .onDelete() modifier to our List view:

List {
ForEach(todos, id: \.self) { todo in
// ... list item code here
}
.onDelete(perform: delete)
}

Now you can perform delete action for any todo list item just by swiping to the left!

Edit / Update item from Core Data

We will utilize TodoInputForm for editing our todo list item. We will display it with a sheet, so first make a Statevariable to indicate whether the sheet is active or not:

@State var isShowingEditForm: Bool = false

Then add .sheet modifier in our root VStack and Edit toolbar button:

VStack {
// stack content here
}
.toolbar {
Button("Edit") {
isShowingEditForm.toggle()
}
}
.sheet(isPresented: $isShowingEditForm) {
TodoInputForm(todo: todo, isPresented: $isShowingEditForm)
}

The update TodoDetailView will look like this:

import SwiftUI

struct TodoDetailView: View {

@State var isShowingEditForm: Bool = false

var todo: Todo

var body: some View {
VStack(alignment: .leading, spacing: 10) {
Text(todo.title ?? "")
.font(.title)
Text(formatDate(todo.date ?? Date()))
.font(.subheadline)
.foregroundColor(.gray)
StatusIndicator(status: todo.status == "completed" ? .completed : .pending)
}
.padding()
.navigationTitle("Todo Details")
.toolbar {
Button("Edit") {
isShowingEditForm.toggle()
}
}
.toolbar {
Button("Edit") {
isShowingEditForm.toggle()
}
}
.sheet(isPresented: $isShowingEditForm) {
TodoInputForm(todo: todo, isPresented: $isShowingEditForm)
}
}

private func formatDate(_ date: Date) -> String {
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .short
return formatter.string(from: date)
}
}

You will get this error, we will handle it on the next part:

Todo List Edit Form

To enable editing or updating a todo item from Core Data using the TodoInputForm, follow these steps:

To handle updating the todo item, add a new State variable in TodoInputForm:

@State var todo: Todo?

In the Form view, add the .onAppear() code to populate the fields with existing data if the form is for editing:

Form {
// ...
}
.onAppear {
if let todo = todo {
self.title = todo.title!
self.date = todo.date!
self.status = todo.status! == "completed" ? .completed : .pending
}
}

Lastly, update the saveTodo function to support Core Data update operation:

// MARK: Core Data Operations
func saveTodo(title: String, date: Date, status: String) {
if todo == nil {
todo = Todo(context: self.viewContext)
todo?.id = UUID()
}
todo?.title = title
todo?.date = date
todo?.status = status

do {
try self.viewContext.save()
print("Todo saved successfully.")
} catch {
print("Error saving todo: \\(error.localizedDescription)")
}
}

Here is your final code in TodoInputForm:

import SwiftUI

struct TodoInputForm: View {
// MARK: Core data variables
@EnvironmentObject var manager: DataManager
@Environment(\.managedObjectContext) var viewContext
@State var todo: Todo?

@Binding var isPresented: Bool
@State private var title: String = ""
@State private var date: Date = Date()
@State private var status: TodoStatus = .pending

var body: some View {
NavigationView {
Form {
Section {
TextField("Enter todo title", text: $title)
} header: {
Text("Title")
}

Section {
DatePicker("Select a date", selection: $date, displayedComponents: .date)
} header: {
Text("Date")
}

Section {
Picker("Select status", selection: $status) {
ForEach(TodoStatus.allCases, id: \.self) { status in
Text(status.rawValue)
}
}
.pickerStyle(SegmentedPickerStyle())
} header: {
Text("Status")
}

}
.navigationTitle("New Todo")
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {
isPresented = false
}
}

ToolbarItem(placement: .navigationBarTrailing) {
Button("Save") {
// Save the data and dismiss the sheet
self.saveTodo(title: title, date: date, status: status.rawValue)
// Call a function to handle saving or further processing of the newTodo
// For example, you can pass it to a delegate or callback.
isPresented = false
}
}
}
.onAppear {
if let todo = todo {
self.title = todo.title!
self.date = todo.date!
self.status = todo.status! == "completed" ? .completed : .pending
}
}
}
}

// MARK: Core Data Operations
func saveTodo(title: String, date: Date, status: String) {
if todo == nil {
todo = Todo(context: self.viewContext)
todo?.id = UUID()
}
todo?.title = title
todo?.date = date
todo?.status = status

do {
try self.viewContext.save()
print("Todo saved!")
} catch {
print("whoops \(error.localizedDescription)")
}
}
}

Wohoo..! Finally we finished! Try to run the project. Now it can save, fetch, update and delete data.

Wrap up

Congratulations! 🎉 You’ve successfully completed the SwiftUI Core Data Tutorial, and now you have a fully functional Todo List app with Core Data integration. Throughout this tutorial, you’ve learned essential concepts like saving, fetching, updating, and deleting data using Core Data within a SwiftUI project.

Finish Project on Github

--

--

Rizal Hilman

Tech Mentor at Apple Developer Academy - Batam | Apple Swift Certified Trainer | Apple Professional Learning Specialist | Apple Teacher | WWDC19 Winner