Day 18 – Adding Persistence to Your To-Do App
T
Tuan Beo

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.swift with PersistenceController


2. Create Core Data Entity

  • Open .xcdatamodeld

  • Add 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 add Codable + save/load logic in ViewModel.

  • For Core Data → Replace [TaskItem] with [TaskEntity] and adjust ForEach and binding.


🛠 Practice

  1. Convert your Day 17 To-Do app to UserDefaults persistence.

  2. Try Core Data by adding a priority field and sorting tasks by it.

  3. In Core Data, add a filter toggle for show only overdue tasks using NSPredicate.

Comments