Swift Task Board Project
A task board project is a small program that stores tasks and prints a summary of the board. The program grows step by step and each step stays complete.
Tasks with a status that carries values
The Status enum carries an owner for an active task and points for a finished task. Each task builds one line from its title and its status.
enum Status {
case pending
case active(owner: String)
case finished(points: Int)
}
struct Task {
var title: String
var status: Status
func statusName() -> String {
switch status {
case .pending:
return "pending"
case .active:
return "active"
case .finished:
return "finished"
}
}
func line() -> String {
switch status {
case .pending:
return "\(title) - pending"
case .active(let owner):
return "\(title) - active (\(owner))"
case .finished(let points):
return "\(title) - finished (\(points) points)"
}
}
}
let tasks = [
Task(title: "Design", status: .finished(points: 5)),
Task(title: "Build", status: .active(owner: "Ana")),
Task(title: "Test", status: .pending)
]
for task in tasks {
print(task.line())
}A default description from a protocol extension
Task adds no description of its own, so it uses the default from the extension. Board writes its own description and that version wins for a board.
enum Status {
case pending
case active(owner: String)
case finished(points: Int)
}
protocol Describable {
func describe() -> String
}
extension Describable {
func describe() -> String {
return "A task for the board"
}
}
struct Task: Describable {
var title: String
var status: Status
}
struct Board: Describable {
var name: String
var tasks: [Task]
func describe() -> String {
return "\(name) board with \(tasks.count) tasks"
}
}
let tasks = [
Task(title: "Design", status: .finished(points: 5)),
Task(title: "Build", status: .active(owner: "Ana"))
]
let board = Board(name: "Sprint", tasks: tasks)
print(board.describe())
print(tasks[0].describe())A throwing add that a caller catches
The add function throws when the title is empty. The second call throws, the catch block prints its line, and the board keeps only the first task.
enum Status {
case pending
case active(owner: String)
case finished(points: Int)
}
enum BoardError: Error {
case emptyTitle
}
struct Task {
var title: String
var status: Status
func line() -> String {
switch status {
case .pending:
return "\(title) - pending"
case .active(let owner):
return "\(title) - active (\(owner))"
case .finished(let points):
return "\(title) - finished (\(points) points)"
}
}
}
struct Board {
var tasks: [Task] = []
mutating func add(title: String, status: Status) throws {
guard !title.isEmpty else {
throw BoardError.emptyTitle
}
tasks.append(Task(title: title, status: status))
}
}
var board = Board()
do {
try board.add(title: "Design", status: .pending)
try board.add(title: "", status: .pending)
} catch {
print("Could not add task")
}
print(board.tasks.count)
for task in board.tasks {
print(task.line())
}Counting by status and filtering with a closure
The default subscript starts each new status count at zero before the addition. The filter closure keeps only the active task and the sorted keys fix the count order.
enum Status {
case pending
case active(owner: String)
case finished(points: Int)
}
struct Task {
var title: String
var status: Status
func statusName() -> String {
switch status {
case .pending:
return "pending"
case .active:
return "active"
case .finished:
return "finished"
}
}
}
let tasks = [
Task(title: "Design", status: .finished(points: 5)),
Task(title: "Build", status: .active(owner: "Ana")),
Task(title: "Test", status: .pending)
]
var counts: [String: Int] = [:]
for task in tasks {
counts[task.statusName(), default: 0] += 1
}
for key in counts.keys.sorted() {
print("\(key): \(counts[key] ?? 0)")
}
let activeTasks = tasks.filter { $0.statusName() == "active" }
for task in activeTasks {
print("Active: \(task.title)")
}Example
enum Status {
case pending
case active(owner: String)
case finished(points: Int)
}
enum BoardError: Error {
case emptyTitle
}
protocol Describable {
func describe() -> String
}
extension Describable {
func describe() -> String {
return "A task for the board"
}
}
struct Task: Describable {
var title: String
var status: Status
func statusName() -> String {
switch status {
case .pending:
return "pending"
case .active:
return "active"
case .finished:
return "finished"
}
}
func line() -> String {
switch status {
case .pending:
return "\(title) - pending"
case .active(let owner):
return "\(title) - active (\(owner))"
case .finished(let points):
return "\(title) - finished (\(points) points)"
}
}
}
struct Board: Describable {
var name: String
var tasks: [Task] = []
mutating func add(title: String, status: Status) throws {
guard !title.isEmpty else {
throw BoardError.emptyTitle
}
tasks.append(Task(title: title, status: status))
}
func describe() -> String {
return "\(name) board with \(tasks.count) tasks"
}
}
var board = Board(name: "Sprint")
do {
try board.add(title: "Design", status: .finished(points: 5))
try board.add(title: "Build", status: .active(owner: "Ana"))
try board.add(title: "Test", status: .pending)
} catch {
print("Could not add task")
}
do {
try board.add(title: "", status: .pending)
} catch {
print("Could not add task")
}
print(board.describe())
for task in board.tasks {
print(task.line())
}
var counts: [String: Int] = [:]
for task in board.tasks {
counts[task.statusName(), default: 0] += 1
}
for key in counts.keys.sorted() {
print("\(key): \(counts[key] ?? 0)")
}
let activeTasks = board.tasks.filter { $0.statusName() == "active" }
for task in activeTasks {
print("Active: \(task.title)")
}
print("Task label: \(board.tasks[0].describe())")Key points
- A Status enum carries an owner name with one case and a points value with another case.
- A Task struct holds a title and a status and builds one board line.
- An array stores the tasks in the order they were added.
- A protocol extension gives a default description to a type that adds none of its own.
- A throwing add function rejects an empty title and a caller catches that error.
- A dictionary counts tasks per status with a default value for each new key.
- A closure passed to filter keeps only the tasks that match.
In this example, the board adds three tasks and the empty title call throws and lands in the catch block. The describe line for the board names Sprint and counts three tasks. Each task then prints its line in the order it was added. The counts walk the sorted keys, so active, finished and pending print in that order with a count of one each. The filter closure keeps only the active task and prints its title. The last line calls describe on the first task, which uses the default from the protocol extension.
Note
Walking the sorted count keys keeps the summary lines in the same order every run.
Try it Yourself
Edit the code, then press Run.
Running is simulated
Exercise
In the finished board, what does counts[task.statusName(), default: 0] += 1 do when a status key is seen for the first time?