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.
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.
- Click “Add Entity.”
- Name the entity “Task.”
- 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.
- Set the id’s attribute property to be a Unique Identifier (UUID).
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 bycreatedAt
.- The body renders a
List
ofTaskRow
views. - The
addTask
function creates a newTask
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 theTask
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
- Run the app on an iOS simulator or a physical device.
- Add tasks by typing into the text field and pressing the return/enter button.
- Swipe left to delete tasks.
- Observe that tasks persist even after closing and reopening the app.
- Checkmark the circle to complete a to-do.
- You may also edit/rename tasks after they have been added by tapping on it’s
TextField
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.