JSTAcademy
0 XP
Dashboard
Crash Courses
Capstone: Task Manager with Persistence
25 min
PhD+200 XP
Crash Courses · PhD

Capstone: Task Manager with Persistence

Build a complete task manager with full CRUD, priority sorting, filtering, JSON persistence to disk, and a clean layered architecture.
25 min read+200 XP on completionCert: Swift Crash Course
Tap any word in the text below to start reading from there.

Capstone: Task Manager with Persistence

Architecture

Task (struct, Codable, Identifiable)
├── id: UUID
├── title: String
├── notes: String?
├── priority: Priority (enum)
├── isCompleted: Bool
├── createdAt: Date
└── dueDate: Date?

TaskStore (actor)
├── add(_ task: Task)
├── update(_ task: Task)
├── delete(id: UUID) -> Bool
├── toggle(id: UUID)
├── all() -> [Task]
├── filtered(by: Filter) -> [Task]
├── sorted(by: SortOrder) -> [Task]
├── save() throws
└── load() throws

Filter enum: all, active, completed, highPriority, overdue
SortOrder enum: byCreated, byDue, byPriority, byTitle

---

The Task Model

import Foundation

enum Priority: Int, Codable, CaseIterable, Comparable {
    case low = 1, medium = 2, high = 3

    static func < (lhs: Priority, rhs: Priority) -> Bool {
        lhs.rawValue < rhs.rawValue
    }

    var label: String {
        switch self {
        case .low:    return "Low"
        case .medium: return "Medium"
        case .high:   return "High"
        }
    }
}

struct Task: Codable, Identifiable, Equatable {
    let id: UUID
    var title: String
    var notes: String?
    var priority: Priority
    var isCompleted: Bool
    let createdAt: Date
    var dueDate: Date?

    init(title: String, notes: String? = nil, priority: Priority = .medium, dueDate: Date? = nil) {
        self.id          = UUID()
        self.title       = title
        self.notes       = notes
        self.priority    = priority
        self.isCompleted = false
        self.createdAt   = Date()
        self.dueDate     = dueDate
    }

    var isOverdue: Bool {
        guard let due = dueDate, !isCompleted else { return false }
        return due < Date()
    }

    var displayStatus: String {
        if isCompleted { return "✓" }
        if isOverdue   { return "!" }
        return "○"
    }
}

---

TaskStore Actor

enum TaskFilter {
    case all, active, completed, highPriority, overdue
}

enum TaskSortOrder {
    case byCreated, byDue, byPriority, byTitle
}

actor TaskStore {
    private var tasks: [UUID: Task] = [:]
    private let saveURL: URL

    init() {
        let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
        self.saveURL = docs.appendingPathComponent("tasks.json")
        // Load existing tasks on init (blocking here for simplicity)
        if let data = try? Data(contentsOf: saveURL),
           let saved = try? JSONDecoder().decode([Task].self, from: data) {
            self.tasks = Dictionary(uniqueKeysWithValues: saved.map { ($0.id, $0) })
        }
    }

    // CRUD
    func add(_ task: Task) {
        tasks[task.id] = task
    }

    func update(_ task: Task) -> Bool {
        guard tasks[task.id] != nil else { return false }
        tasks[task.id] = task
        return true
    }

    func delete(id: UUID) -> Bool {
        return tasks.removeValue(forKey: id) != nil
    }

    func toggle(id: UUID) -> Bool {
        guard var task = tasks[id] else { return false }
        task.isCompleted.toggle()
        tasks[id] = task
        return true
    }

    // Queries
    func all() -> [Task] {
        Array(tasks.values)
    }

    func filtered(by filter: TaskFilter) -> [Task] {
        let all = Array(tasks.values)
        switch filter {
        case .all:           return all
        case .active:        return all.filter { !$0.isCompleted }
        case .completed:     return all.filter {  $0.isCompleted }
        case .highPriority:  return all.filter {  $0.priority == .high }
        case .overdue:       return all.filter {  $0.isOverdue }
        }
    }

    func sorted(by order: TaskSortOrder) -> [Task] {
        let all = Array(tasks.values)
        switch order {
        case .byCreated:
            return all.sorted { $0.createdAt < $1.createdAt }
        case .byDue:
            return all.sorted {
                switch ($0.dueDate, $1.dueDate) {
                case (nil, nil):   return false
                case (nil, _):    return false  // no due date sorts last
                case (_, nil):    return true
                case (let a?, let b?): return a < b
                }
            }
        case .byPriority:
            return all.sorted { $0.priority > $1.priority }  // high first
        case .byTitle:
            return all.sorted { $0.title.localizedCaseInsensitiveCompare($1.title) == .orderedAscending }
        }
    }

    // Persistence
    func save() throws {
        let encoder = JSONEncoder()
        encoder.dateEncodingStrategy = .iso8601
        encoder.outputFormatting = .prettyPrinted
        let data = try encoder.encode(Array(tasks.values))
        try data.write(to: saveURL)
    }

    func stats() -> (total: Int, active: Int, completed: Int, overdue: Int) {
        let all = Array(tasks.values)
        return (
            total:     all.count,
            active:    all.filter { !$0.isCompleted }.count,
            completed: all.filter {  $0.isCompleted }.count,
            overdue:   all.filter {  $0.isOverdue   }.count
        )
    }
}

---

Main Program

Task {
    let store = TaskStore()

    // Add tasks
    let tomorrow = Calendar.current.date(byAdding: .day, value: 1, to: Date())!
    let yesterday = Calendar.current.date(byAdding: .day, value: -1, to: Date())!

    await store.add(Task(title: "Launch JST Academy",  priority: .high,   dueDate: tomorrow))
    await store.add(Task(title: "Deploy Ferguson Law",  priority: .high,   dueDate: yesterday))
    await store.add(Task(title: "Review design assets", priority: .medium))
    await store.add(Task(title: "Update client report", priority: .low))

    // Toggle first two as done
    let all = await store.all()
    for task in all.prefix(1) {
        await store.toggle(id: task.id)
    }

    // Print stats
    let s = await store.stats()
    print("Stats: \(s.total) total, \(s.active) active, \(s.completed) done, \(s.overdue) overdue")

    // Print by priority
    print("\nBy Priority (high first):")
    for task in await store.sorted(by: .byPriority) {
        print("  [\(task.displayStatus)] \(task.priority.label.uppercased()): \(task.title)")
    }

    // Save to disk
    do {
        try await store.save()
        print("\nSaved to disk successfully")
    } catch {
        print("Save failed: \(error)")
    }
}
0%