
Day 18 – Adding Persistence to Your To-Do App
Save and load app data using UserDefaults or Core Data so the user’s tasks remain after closing the app.
1️⃣ Option A – Quick Save with UserDefaults
Perfect for small apps or prototypes where:
Data is small
No complex relationships
Performance is not critical
1. Extend ViewModel
We’ll make TasksViewModel save tasks automatically.
final class TasksViewModel: ObservableObject {
@Published var tasks: [TaskItem] = [] {
didSet { saveTasks() }
}
private let saveKey = "tasks_storage_key"
init() {
loadTasks()
}
private func saveTasks() {
if let data = try? JSONEncoder().encode(tasks) {
UserDefaults.standard.set(data, forKey: saveKey)
}
}
private func loadTasks() {
guard let data = UserDefaults.standard.data(forKey: saveKey),
let saved = try? JSONDecoder().decode([TaskItem].self, from: data) else { return }
tasks = saved
}
}
2. Make TaskItem Codable
struct TaskItem: Identifiable, Hashable, Codable {
let id: UUID
var title: String
var notes: String
var isDone: Bool
var due: Date?
}
Pros:
✅ Very easy to implement
✅ No external dependencies
Cons:
❌ All data must fit in memory
❌ Not good for large datasets or complex queries
2️⃣ Option B – Core Data (Scalable Storage)
Best for:
Large datasets
Relationships (e.g., projects → tasks)
Sorting/filtering at the database level
1. Enable Core Data in Xcode
Go to your project settings → check Use Core Data (or add manually)
Xcode creates
Persistence.swiftwithPersistenceController
2. Create Core Data Entity
Open
.xcdatamodeldAdd entity: TaskEntity
id(UUID)title(String)notes(String)isDone(Boolean)due(Date, optional)
3. ViewModel using Core Data
import CoreData
final class TasksViewModel: ObservableObject {
@Published var tasks: [TaskEntity] = []
private let context = PersistenceController.shared.container.viewContext
init() {
fetchTasks()
}
func fetchTasks() {
let request: NSFetchRequest<TaskEntity> = TaskEntity.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(keyPath: \TaskEntity.due, ascending: true)]
tasks = (try? context.fetch(request)) ?? []
}
func addTask(title: String, notes: String, isDone: Bool, due: Date?) {
let newTask = TaskEntity(context: context)
newTask.id = UUID()
newTask.title = title
newTask.notes = notes
newTask.isDone = isDone
newTask.due = due
saveContext()
fetchTasks()
}
func updateTask(_ task: TaskEntity) {
saveContext()
fetchTasks()
}
func deleteTask(_ task: TaskEntity) {
context.delete(task)
saveContext()
fetchTasks()
}
private func saveContext() {
if context.hasChanges {
try? context.save()
}
}
}
3️⃣ Choosing Between UserDefaults & Core Data
Feature | UserDefaults | Core Data |
|---|---|---|
Setup | Very simple | More setup |
Data size | Small | Large |
Relationships | No | Yes |
Query/filter | Manual in code | Built-in |
Performance | Fine for small | Optimized for big data |
4️⃣ Integration in To-Do App
For UserDefaults → You already have
tasks: [TaskItem]from Day 17 → just addCodable+ save/load logic in ViewModel.For Core Data → Replace
[TaskItem]with[TaskEntity]and adjustForEachand binding.
🛠 Practice
Convert your Day 17 To-Do app to UserDefaults persistence.
Try Core Data by adding a
priorityfield and sorting tasks by it.In Core Data, add a filter toggle for show only overdue tasks using
NSPredicate.
