Building a To-Do List with Core Data and SwiftUI

In modern iOS app development, managing data efficiently is critical. Core Data, Apple’s persistent data framework, coupled with SwiftUI, Apple’s declarative UI framework, offers a powerful and elegant solution for creating data-driven applications. In this guide, we’ll explore how to build a simple yet functional To-Do list app using Core Data and SwiftUI.

Understanding Core Data

Core Data is Apple’s framework for managing the model layer objects in an application. It provides generalized and automated solutions to common tasks associated with object lifecycle management and persistence, making it a robust choice for data storage on Apple platforms.

Benefits of Using Core Data

  • Data Persistence: Seamlessly store and retrieve data.
  • Object Graph Management: Manage relationships between data entities efficiently.
  • Undo and Redo: Support undo and redo operations with ease.
  • Data Validation: Ensure data integrity through validation rules.
  • Performance: Optimize data access and manipulation.

Setting Up Core Data in Your SwiftUI Project

Step 1: Create a New Xcode Project

Start by creating a new Xcode project. Select the “App” template under the iOS tab, name your project (e.g., “ToDoListApp”), ensure that the interface is set to “SwiftUI,” and check the “Use Core Data” box.

\"Create

Step 2: Examine the Core Data Stack

Xcode automatically sets up the Core Data stack for you. The necessary code is in the [YourAppName]App.swift file. Here’s what you’ll typically find:


import SwiftUI
import CoreData

@main
struct ToDoListApp: App {
    let persistenceController = PersistenceController.shared

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(\\Environment(\\ManagedObjectContext.self, persistenceController.container.viewContext)
        }
    }
}

And the PersistenceController.swift typically looks like this:


import CoreData

struct PersistenceController {
    static let shared = PersistenceController()

    let container: NSPersistentContainer

    init(inMemory: Bool = false) {
        container = NSPersistentContainer(name: "ToDoListApp")
        if inMemory {
            container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
        }
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                fatalError("Unresolved error (error.localizedDescription), (error.userInfo)")
            }
        })
        container.viewContext.automaticallyMergesChangesFromParent = true
    }
}

Key components of the Core Data stack:

  • NSPersistentContainer: Encapsulates the Core Data stack components, making it easier to manage.
  • NSManagedObjectContext: An in-memory “scratchpad” for working with managed objects. You’ll interact with this to fetch, create, and save data.
  • NSManagedObjectModel: Describes the data model (entities, attributes, relationships).
  • NSPersistentStoreCoordinator: Mediates between the managed object context and the persistent store.

Step 3: Define Your Data Model

Open the [YourAppName].xcdatamodeld file (Core Data model file). This is where you define your app’s data entities.

  1. Click “Add Entity.”
  2. Name the entity “Task.”
  3. Add attributes to the Task entity:
    • id: UUID, is used to provide unique identifiers.
    • name: String, stores the task description.
    • createdAt: Date, registers when task was created.
    • isCompleted: Boolean, used to keep track of which tasks have been completed.
  4. Set the id’s attribute property to be a Unique Identifier (UUID).

\"Core

Step 4: Generate Managed Object Subclass

Select the ‘Task’ entity in the Core Data model editor. Then in Xcode go to Editor > Create NSManagedObject Subclass… and follow the prompts, selecting the “Task” entity and confirming the default options, to create the Task.swift. The swift file will extend the generated managed object by including a task class in the project.


import Foundation
import CoreData

@objc(Task)
public class Task: NSManagedObject {

}

extension Task {

    @nonobjc public class func fetchRequest() -> NSFetchRequest {
        return NSFetchRequest(entityName: "Task")
    }

    @NSManaged public var createdAt: Date?
    @NSManaged public var id: UUID?
    @NSManaged public var isCompleted: Bool
    @NSManaged public var name: String?

}

extension Task : Identifiable {

}

Building the SwiftUI User Interface

Step 1: Create the ContentView

Update ContentView.swift to display a list of tasks and an interface to add new tasks. Start by fetching tasks from Core Data.


import SwiftUI
import CoreData

struct ContentView: View {
    @Environment(\\Environment(\\ManagedObjectContext.self) private var viewContext
    
    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \\Task.createdAt, ascending: true)],
        animation: .default)
    private var tasks: FetchedResults

    @State private var newTaskName: String = ""

    var body: some View {
        NavigationView {
            List {
                ForEach(tasks) { task in
                    TaskRow(task: task)
                }
                .onDelete(perform: deleteTask)
            }
            .navigationTitle("To-Do List")
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    EditButton()
                }
                ToolbarItem {
                    Button(action: addTask) {
                        Label("Add Item", systemImage: "plus")
                    }
                }
            }
            .overlay {
                if tasks.isEmpty {
                    ContentUnavailableView {
                        Label("No Tasks", systemImage: "list.bullet.circle")
                    } description: {
                        Text("Add Tasks to see them here")
                    }
                }
            }
        }
    }

    private func addTask() {
        withAnimation {
            let newTask = Task(context: viewContext)
            newTask.id = UUID()
            newTask.name = newTaskName
            newTask.createdAt = Date()
            newTask.isCompleted = false

            do {
                try viewContext.save()
            } catch {
                let nsError = error as NSError
                fatalError("Unresolved error (nsError), (nsError.userInfo)")
            }
            newTaskName = "" // resets newTaskName for next usage.
        }
    }

    private func deleteTask(offsets: IndexSet) {
        withAnimation {
            offsets.map { tasks[$0] }.forEach(viewContext.delete)

            do {
                try viewContext.save()
            } catch {
                let nsError = error as NSError
                fatalError("Unresolved error (nsError), (nsError.userInfo)")
            }
        }
    }
}

Key aspects:

  • @Environment(\\Environment(\\ManagedObjectContext.self) private var viewContext: Accesses the managed object context.
  • @FetchRequest: Fetches tasks sorted by createdAt.
  • The body renders a List of TaskRow views.
  • The addTask function creates a new Task and saves it to Core Data.
  • The deleteTask function deletes selected tasks from Core Data.
  • Empty Content List displays an unavailable View which gives guidance to the user.

Step 2: Create TaskRow View

Create a new SwiftUI file named TaskRow.swift. This view represents each task in the list.


import SwiftUI
import CoreData

struct TaskRow: View {
    @ObservedObject var task: Task

    var body: some View {
        HStack {
            Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle")
                .foregroundColor(task.isCompleted ? .green : .gray)
                .onTapGesture {
                    task.isCompleted.toggle()
                    saveContext()
                }
            TextField("New Task", text: Binding(
                get: { task.name ?? "" },
                set: { newValue in
                    task.name = newValue
                    saveContext()
                }
            ))
            .textFieldStyle(.plain) // Make TextField style to a plain unbordered style.
        }
    }

    // save view context.
    private func saveContext() {
        do {
            try task.managedObjectContext?.save()
        } catch {
            let nsError = error as NSError
            fatalError("Unresolved error (nsError), (nsError.userInfo)")
        }
    }
}

Key aspects:

  • @ObservedObject var task: Task: Watches for changes in the Task object.
  • Tapping the task will save if it is complete.
  • TextField provides interface to rename any current tasks.
  • When there are changes to the task, the context will also save.

Step 3: Create the New Task View

Since users can edit the To-Do List items. Update the content view file.


import SwiftUI
import CoreData

struct ContentView: View {
    @Environment(\\Environment(\\ManagedObjectContext.self) private var viewContext
    
    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \\Task.createdAt, ascending: true)],
        animation: .default)
    private var tasks: FetchedResults

    @State private var newTaskName: String = ""

    var body: some View {
        NavigationView {
            VStack {
                // Textfield
                TextField("Add New Task", text: $newTaskName)
                    .padding()
                    .onSubmit(addTask) // on hitting enter, the addTask Function is performed.

                List {
                    ForEach(tasks) { task in
                        TaskRow(task: task)
                    }
                    .onDelete(perform: deleteTask)
                }
                .listStyle(.plain) // styles the list plain, and unbordered.
                .overlay {
                    if tasks.isEmpty {
                        ContentUnavailableView {
                            Label("No Tasks", systemImage: "list.bullet.circle")
                        } description: {
                            Text("Add Tasks to see them here")
                        }
                    }
                }
            }
            .navigationTitle("To-Do List")
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    EditButton()
                }
            }
            .padding()
        }
    }

    private func addTask() {
        withAnimation {
            let newTask = Task(context: viewContext)
            newTask.id = UUID()
            newTask.name = newTaskName
            newTask.createdAt = Date()
            newTask.isCompleted = false

            do {
                try viewContext.save()
            } catch {
                let nsError = error as NSError
                fatalError("Unresolved error (nsError), (nsError.userInfo)")
            }
            newTaskName = "" // resets newTaskName for next usage.
        }
    }

    private func deleteTask(offsets: IndexSet) {
        withAnimation {
            offsets.map { tasks[$0] }.forEach(viewContext.delete)

            do {
                try viewContext.save()
            } catch {
                let nsError = error as NSError
                fatalError("Unresolved error (nsError), (nsError.userInfo)")
            }
        }
    }
}

Here are the key differences in the code

  • Included Textfield to take input of to-do tasks, TextField("Add New Task", text: $newTaskName)
  • TextField will take the current input, with onSubmit(addTask) performing action of submission with a carriage return (enter button.)
  • the original navigationBarTrailing code has been refactored to the body.

Testing the App

  1. Run the app on an iOS simulator or a physical device.
  2. Add tasks by typing into the text field and pressing the return/enter button.
  3. Swipe left to delete tasks.
  4. Observe that tasks persist even after closing and reopening the app.
  5. Checkmark the circle to complete a to-do.
  6. You may also edit/rename tasks after they have been added by tapping on it’s TextField

\"Running

Conclusion

Building a To-Do list app using Core Data and SwiftUI demonstrates the ease and power of combining Apple’s modern frameworks. Core Data provides robust data persistence, while SwiftUI offers a declarative and efficient way to build the user interface. This combination allows developers to create engaging, data-driven applications with less code and greater maintainability. Embrace Core Data and SwiftUI to streamline your iOS app development and create exceptional user experiences.