EduFlow

Section 1 · Swift the Language

Functions and Closures

18 min3 min readFill in the blanks

Lesson at a glance

Write functions with clear argument labels and default values, then replace hand-written loops with closures passed to filter, map, reduce and sorted.

  1. 1Functions that read like sentences
  2. 2Closures: functions without names
  3. 3filter, map, reduce, sorted
  4. 4Closures capture their surroundings

No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.

The for loops from the last lesson work, but they make you read five lines to learn one fact, such as “the total spent on food”. Functions give a piece of logic a name. Closures let you hand a small piece of logic to someone else, like “keep the ones over 50”, and Swift’s collection methods are built around them. Together they turn most loops in Pocket Budget into one readable line.

For the examples, here is a minimal expense type. You’ll learn what struct really means in the next section; for now, read it as “a value with a title, an amount and a category”.

Expense.swift
import Foundation

struct Expense {
    let title: String
    let amount: Decimal
    let category: String
}

let expenses = [
    Expense(title: "Groceries", amount: 64.2, category: "Food"),
    Expense(title: "Bus pass", amount: 45, category: "Transport"),
    Expense(title: "Coffee", amount: 4.5, category: "Food"),
    Expense(title: "Concert", amount: 80, category: "Fun"),
]

Functions that read like sentences

Swift
func total(of expenses: [Expense], in category: String? = nil) -> Decimal {
    var sum: Decimal = 0
    for expense in expenses where category == nil || expense.category == category {
        sum += expense.amount
    }
    return sum
}

total(of: expenses)                  // 193.7
total(of: expenses, in: "Food")      // 68.7

Each parameter has two names. The argument label (of, in) is what callers write; the parameter name (expenses, category) is what the body uses. That split is why Swift call sites read like English: “total of expenses in Food”. The = nil gives category a default value, so callers can leave it out. Write _ as the label when it adds nothing, as in print(_:).

The -> Decimal declares the return type. A function that returns nothing omits the arrow.

Closures: functions without names

A closure is a block of code you can store in a constant or pass as an argument. Here is a closure that decides whether an expense is large:

Swift
let isLarge: (Expense) -> Bool = { expense in
    expense.amount > 50
}
isLarge(expenses[0])     // true

The type (Expense) -> Bool reads “takes an Expense, returns a Bool”. Parameters go before in, the body after it. A single-expression body returns its value without writing return.

Now pass logic like that to filter. These three lines are the same call, progressively shortened:

Swift
let large1 = expenses.filter({ (expense: Expense) -> Bool in
    return expense.amount > 50
})
let large2 = expenses.filter { expense in expense.amount > 50 }
let large3 = expenses.filter { $0.amount > 50 }

When a closure is the last argument, you can write it after the parentheses (trailing closure syntax) and drop empty parentheses entirely. Swift already knows the types from filter’s signature, so you can drop those too. $0 is the first parameter, $1 the second.

filter, map, reduce, sorted

These four cover most of what you do with collections:

Swift
let food = expenses.filter { $0.category == "Food" }        // keep matching expenses
let titles = food.map { $0.title }                           // ["Groceries", "Coffee"]
let foodTotal = food.reduce(0) { $0 + $1.amount }            // 68.7
let biggestFirst = expenses.sorted { $0.amount > $1.amount } // Concert, Groceries, ...

reduce starts from an initial value (0) and folds each element into a running result: $0 is the total so far and $1 is the next expense. sorted(by:) asks your closure “should $0 come before $1?”.

Chain them, and the pipeline reads in the order it runs:

Swift
let foodSpend = expenses
    .filter { $0.category == "Food" }
    .map(\.amount)
    .reduce(0, +)

\.amount is a key path, a shorthand for { $0.amount }, and + is itself a function that takes two values and returns their sum, so you can pass it straight to reduce.

Four expenses flow into filter, which keeps the two food expenses; map turns them into two amounts; reduce adds them into one total of 68.7. 4 expenses [Expense] filter 2 food items map(\.amount) [64.2, 4.5] reduce 68.7 Each step returns a new value; the original array never changes
filter, map and reduce as a pipeline

Closures capture their surroundings

A closure can use variables from the scope where it was created, and it keeps them alive:

Swift
func makeLimitCheck(limit: Decimal) -> (Decimal) -> Bool {
    return { amount in amount > limit }
}

let overFoodLimit = makeLimitCheck(limit: 60)
overFoodLimit(64.2)     // true

makeLimitCheck has returned, yet the closure still knows limit is 60. When a function stores a closure parameter to call later, instead of calling it before returning, Swift makes you mark that parameter @escaping. The keyword is a warning label for readers: this closure outlives the call, so whatever it captures stays alive too.

This capturing is what lets SwiftUI buttons run code that refers to your view’s data long after the view was built. It has one sharp edge, a reference cycle when a class and a closure capture each other, which you’ll meet with classes in the next lesson.

You can now name logic, pass it around and replace loops with pipelines. That finishes the core language. The next section is about modelling: deciding whether Pocket Budget’s data should be structs, classes or enums, and why that choice changes how your app behaves.

Key takeaways

  • Argument labels make call sites read like sentences; use _ only when the label would add nothing.
  • A closure is a function without a name, and trailing closure syntax lets you write it after the call’s parentheses.
  • filter keeps elements, map transforms them, reduce combines them into one value, and sorted(by:) returns a new ordered array.
  • A closure captures the variables it uses, so it can carry context such as a limit to wherever it’s called later.

Further reading

Check your understanding

4 questions

Question 1 of 4

Given func total(of expenses: [Expense], in category: String? = nil) -> Decimal, which call compiles?

Question 2 of 4

What does [3, 8, 12].map { $0 * 2 }.filter { $0 > 10 } produce?

Question 3 of 4

You write expenses.sorted { $0.amount > $1.amount } on its own line and nothing changes. Why?

Question 4 of 4

let check = makeLimitCheck(limit: 60) returns a closure { amount in amount > limit }. What does check(64) return, long after makeLimitCheck finished?

Practice

Fill in the blanks

Replace the loops with closures

Pocket Budget’s summary screen needs four values. Fill each blank so that large holds the expenses over 50, titles holds their titles, spent is the total of all amounts, and biggestFirst lists every expense from the largest amount to the smallest.

// expenses: [Expense], each with title, amount and category let large = expenses. { $0.amount > 50 } let titles = large. { $0.title } let spent = expenses.(0) { $0 + $1.amount } let biggestFirst = expenses.sorted { $0.amount $1.amount }