100 Days of SwiftUI Checkpoint 5

100 Days of SwiftUI Checkpoint 5

Course Credit:

This blog post's content is based on the fantastic learning materials from Paul Hudson's 100 Days of SwiftUI course, available for free at Hacking with Swift.


Closures in Swift

Closures are flexible blocks of code that can be passed around and used in your program. Crucially, closures allow you to assign functionality directly to a constant or variable. If functions are like named recipes, closures are like quick, anonymous instructions you can hand off to someone. They are a fundamental part of modern Swift development, essential for writing clean, concise, and powerful code.

Anatomy of a Closure

  • Syntax: Closures are defined within curly braces {}. Unlike functions, they don't have a func keyword or a name.
  • Parameters & Return Values: You list parameters and their return type inside the braces, followed by the inkeyword. The code to be executed follows in.
    • Example: { (name: String) -> String in return "Hello, \(name)" }
  • Assigning to Variables: You can assign a closure to a constant or variable, and then call it just like a regular function.
    • Example: let sayHello = { ... } then sayHello("Taylor")

Key Concepts

Closures as Parameters

A common use of closures is to pass them as arguments to a function to customize its behavior. This is often seen with array methods like sorted()filter(), and map().

  • filter(): This method runs a closure on every element in an array. The closure must return a Bool to indicate whether the element should be included in the new array.
    • Example: let tOnly = team.filter { $0.hasPrefix("T") } finds all names beginning with "T".

Shorthand Syntax

Swift provides several ways to make closures more concise and easier to read.

  • Trailing Closure: If a closure is the last argument to a function, you can write it after the function's parentheses.
    • Example: team.filter { ... } instead of team.filter(by: { ... })
  • Inferred Types & Implicit Return: Swift can often figure out the parameter and return types for you. If a closure has a single line of code, you can also omit the return keyword.
  • Shorthand Argument Names: You can use $0$1$2, and so forth to refer to the closure's parameters by their position, without having to name them explicitly.

Chaining Methods

You can chain multiple closure-based array methods together to perform complex operations in a single, readable line of code. This is a very common pattern in Swift.

  • Example: luckyNumbers.filter { ... }.sorted { ... }.map { ... }
    • First, filter removes unwanted items.
    • Then, sorted organizes the remaining items.
    • Finally, map transforms each item into a new value.
import UIKit

// --- Closures: Functions without a Name ---
// A closure is a self-contained block of functionality that can be passed around and used in your code.
// Think of closures as flexible, lightweight functions.

// --- Functions as Closures ---
// You can assign a function to a variable, just like any other data type.
// This is the simplest way to understand the concept of a closure.

// A regular, named function.
func greetUser() {
    print("Hello, world!")
}

// Call the function the normal way.
greetUser()

// Assign the function to a variable. The type `() -> Void` means "a function that takes no parameters and returns nothing."
var greetCopy: () -> Void = greetUser

// Now we can call the function using our new variable.
greetCopy()

// Another example of assigning a function to a variable.
func getUserData(for id: Int) -> String {
    if id == 1989 {
        return "Taylor Swift"
    } else {
        return "Anonymous"
    }
}

// The type is `(Int) -> String`, meaning it takes an `Int` and returns a `String`.
let data: (Int) -> String = getUserData
let user = data(1989)
print(user)


// --- Basic Closure Syntax ---
// This is how you write a closure directly without a separate function.
// The `in` keyword separates the parameter and return type from the body of the closure.

let sayHello = { (name: String) -> String in
    let greeting = "Hi \(name)!"
    print(greeting)
    return greeting
}

sayHello("Taylor")

// A closure with conditional logic.
var sendMessage = { (message: String) in
    if !message.isEmpty {
        print("Sending to Twitter: \(message)")
    } else {
        print("Your message was empty.")
    }
}

sendMessage("Hello, world!")
sendMessage("")


// --- Closures as Function Parameters ---
// This is a common and powerful use of closures. You can pass a closure to a function to customize its behavior.

let team = ["Gloria", "Suzanne", "Piper", "Tiffany", "Tasha"]

// The `sorted()` method sorts the array alphabetically by default.
let sortedTeam = team.sorted()
print(sortedTeam)

// We can define a separate function and pass it to `sorted(by:)`.
func captainFirstSorted(a: String, b: String) -> Bool {
    if a == "Tiffany" {
        return true
    } else if b == "Tiffany" {
        return false
    }
    return a < b
}

let captainFirstTeam = team.sorted(by: captainFirstSorted)
print(captainFirstTeam)

// The same sorting logic, but written directly as a closure.
let captainFirstTeam1 = team.sorted(by: { (a: String, b: String) -> Bool in
    if a == "Tasha" {
        return true
    } else if b == "Tasha" {
        return false
    }
    return a < b
})
print(captainFirstTeam1)


// --- Trailing Closures and Shorthand Syntax ---
// When a closure is the last parameter to a function, you can use trailing closure syntax.
// Swift can also infer types and provide shorthand argument names like `$0`, `$1`, etc.

let team1 = ["Joe", "Will", "Piper", "Mav", "Digg"]
// Standard closure syntax
let sorted1 = team1.sorted(by: { (a: String, b: String) -> Bool in
    if a == "Piper" { return true }
    if b == "Piper" { return false }
    return a < b
})

// Trailing closure with shorthand syntax and implicit return
let captainFirstTeam2 = team.sorted {
    if $0 == "Tiffany" {
        return true
    } else if $1 == "Tiffany" {
        return false
    }
    return $0 < $1
}
print(captainFirstTeam2)

// Other examples of shorthand syntax with array methods.
let reverseTeam = team.sorted { $0 > $1 }
print("Reverse team: \(reverseTeam)")

let tOnly = team.filter { $0.hasPrefix("T") }
print("Names starting with 'T': \(tOnly)")

let uppercaseTeam = team.map { $0.uppercased() }
print("Uppercase team: \(uppercaseTeam)")


// --- Functions Accepting Closures ---
// You can write your own functions that take closures as parameters for a highly flexible design.

// This function takes a `generator` closure to create an array of numbers.
func makeArray(size: Int, using generator: () -> Int) -> [Int] {
    var numbers = [Int]()
    for _ in 0..<size {
        numbers.append(generator())
    }
    return numbers
}

// Call `makeArray` with a trailing closure to generate 50 random numbers.
let rolls = makeArray(size: 50) {
    Int.random(in: 1...20)
}
print(rolls)

// We can also pass a regular function that matches the closure's type.
func generateNumber() -> Int {
    Int.random(in: 1...20)
}
let newRolls = makeArray(size: 50, using: generateNumber)
print(newRolls)

// A function that takes multiple closures.
func doImportantWork(first: () -> Void, second: () -> Void, third: () -> Void) {
    print("About to start first work....")
    first()
    print("About to start the second work....")
    second()
    print("About to start the third work....")
    third()
    print("All Done!")
}

// Calling the function using a trailing closure for each parameter.
doImportantWork {
    print("This is the first work")
} second: {
    print("This is the second work")
} third: {
    print("This is the third work")
}


// --- Checkpoint 5 - Putting It All Together ---
/*
 This example shows how to chain multiple closure-based array methods.
 We'll filter, sort, and map an array of numbers.
 
 Your job is to:

 1. Filter out any numbers that are even
 2. Sort the array in ascending order
 3. Map them to strings in the format “7 is a lucky number”
 4. Print the resulting array, one item per line
*/

let luckyNumbers = [7, 4, 38, 21, 16, 15, 12, 33, 31, 49]

let result = luckyNumbers.filter { (number: Int) -> Bool in
    return !number.isMultiple(of: 2)
}.sorted { (number1: Int, number2: Int) -> Bool in
    return number1 < number2
}.map { (number: Int) -> String in
    return "\(number) is a lucky number"
}

for item in result {
    print(item)
}

/*
 Recap of Closures:
 - **Functions as values:** You can assign a function or a block of code to a constant or variable.
 - **Closure syntax:** Closures are defined within curly braces `{}`. Parameters and return types are listed inside the braces, followed by the `in` keyword, which separates the declaration from the closure's body.
 - **Flexibility:** Closures are commonly used to provide a "test" or an action for array methods like `filter`, `sorted`, and `map`. This lets you customize an array's behavior without writing a new function.
 - **Trailing closure syntax:** If a closure is the last parameter in a function call, you can write it after the function's parentheses.
 - **Shorthand syntax:** Swift can often infer the parameter and return types for you, allowing you to omit them. You can also use shorthand argument names like `$0`, `$1`, and so on, to refer to parameters, making the code much more concise.
 - **Implicit return:** If a closure contains only a single line of code, you can remove the `return` keyword.
 */