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)")
}
}Reading progress
0% read
In this module
Persistence
UUID
FileManager
Equatable / Hashable
0%