SwiftUI Core Data Tutorial: Integrating Core Data and Building a Todo List App (Save, Delete, Fetch & Update)
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 State
variable 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.