Chapters

Hide chapters

Swift Apprentice: Fundamentals

First Edition · iOS 16 · Swift 5.7 · Xcode 14.2

Section III: Building Your Own Types

Section 3: 9 chapters
Show chapters Hide chapters

8. Collection Iteration With Closures
Written by Matt Galloway

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

Earlier, you learned about functions. But Swift has another object you can use to break up code into reusable chunks: a closure. They become instrumental when dealing with collections.

A closure is simply a function with no name; you can assign it to a variable and pass it around like any other value. This chapter shows you how convenient and valuable closures can be.

Closure Basics

Closures are so named because they can “close over” the variables and constants within the closure’s scope. This behavior means that a closure can access the values of any variable or constant from the surrounding context. Variables and constants used within the closure body are said to have been captured by the closure.

You may ask, “If closures are functions without names, how do you use them?” To use a closure, you must assign it to a variable or constant.

Here’s a declaration of a variable that can hold a closure:

var multiplyClosure: (Int, Int) -> Int

multiplyClosure takes two Int values and returns an Int. Notice that this is the same as a variable declaration for a function. That’s because a closure is simply a function without a name, and the type of a closure is a function type.

For the declaration to compile in a playground, you need to provide an initial definition like so:

var multiplyClosure = { (a: Int, b: Int) -> Int in
  return a * b
}

This code looks similar to a function declaration, but there’s a subtle difference. There’s the same parameter list, -> symbol and return type. But with closures, these elements appear inside braces, and there is an in keyword after the return type.

With your closure variable defined, you can use it just as if it were a function, like so:

let result = multiplyClosure(4, 2)

As you’d expect, result equals 8. Again, though, there’s a subtle difference.

Notice how the closure has no external names for the parameters. You can’t set them like you can with functions.

Shorthand Syntax

There are many ways to shorten the syntax of a closure. First, just like normal functions, if the closure consists of a single return statement, you can leave out the return keyword like so:

multiplyClosure = { (a: Int, b: Int) -> Int in
  a * b
}
multiplyClosure = { (a, b) in
  a * b
}
multiplyClosure = {
  $0 * $1
}
func operateOnNumbers(_ a: Int, _ b: Int,
                      operation: (Int, Int) -> Int) -> Int {
  let result = operation(a, b)
  print(result)
  return result
}
let addClosure = { (a: Int, b: Int) in
  a + b
}
operateOnNumbers(4, 2, operation: addClosure)
func addFunction(_ a: Int, _ b: Int) -> Int {
  a + b
}
operateOnNumbers(4, 2, operation: addFunction)
operateOnNumbers(4, 2, operation: { (a: Int, b: Int) -> Int in
  return a + b
})
operateOnNumbers(4, 2, operation: { $0 + $1 })
operateOnNumbers(4, 2, operation: +)
operateOnNumbers(4, 2) {
  $0 + $1
}

Multiple Trailing Closures Syntax

If a function has multiple closures for inputs, you can call it in a special shorthand way. Suppose you have this function:

func sequenced(first: ()->Void, second: ()->Void) {
  first()
  second()
}
sequenced {
  print("Hello, ", terminator: "")
} second: {
  print("world.")
}

Closures With no Return Value

Until now, all the closures you’ve seen have taken one or more parameters and have returned values. But just like functions, closures aren’t required to do these things. Here’s how you declare a closure that takes no parameters and returns nothing:

let voidClosure: () -> Void = {
  print("Swift Apprentice is awesome!")
}
voidClosure()

Capturing From the Enclosing Scope

Finally, let’s return to the defining characteristic of a closure: it can access the variables and constants within its scope.

var counter = 0
let incrementCounter = {
  counter += 1
}
incrementCounter()
incrementCounter()
incrementCounter()
incrementCounter()
incrementCounter()
func countingClosure() -> () -> Int {
  var counter = 0
  let incrementCounter: () -> Int = {
    counter += 1
    return counter
  }
  return incrementCounter
}
let counter1 = countingClosure()
let counter2 = countingClosure()

counter1() // 1
counter2() // 1
counter1() // 2
counter1() // 3
counter2() // 2

Custom Sorting With Closures

Closures come in handy when you start looking deeper at collections. In Chapter 7, “Arrays, Dictionaries & Sets”, you used array’s sort method to sort an array. By specifying a closure, you can customize how things are sorted. You call sorted() to get a sorted version of the array as so:

let names = ["ZZZZZZ", "BB", "A", "CCCC", "EEEEE"]
names.sorted()
// ["A", "BB", "CCCC", "EEEEE", "ZZZZZZ"]
names.sorted {
  $0.count > $1.count
}
// ["ZZZZZZ", "EEEEE", "CCCC", "BB", "A"]

Iterating Over Collections With Closures

In Swift, collections implement convenient features often associated with functional programming. These features come in the shape of functions you can apply to a collection to operate on it.

let values = [1, 2, 3, 4, 5, 6]
values.forEach {
  print("\($0): \($0*$0)")
}
var prices = [1.5, 10, 4.99, 2.30, 8.19]

let largePrices = prices.filter {
  $0 > 5
}
func filter(_ isIncluded: (Element) -> Bool) -> [Element]
let largePrice = prices.first {
  $0 > 5
}
let salePrices = prices.map {
  $0 * 0.9
}
let userInput = ["0", "11", "haha", "42"]

let numbers1 = userInput.map {
  Int($0)
}
let numbers2 = userInput.compactMap {
  Int($0)
}
let userInputNested = [["0", "1"], ["a", "b", "c"], ["🐕"]]
let allUserInput = userInputNested.flatMap {
  $0
}
let sum = prices.reduce(0) {
  $0 + $1
}
let stock = [1.5: 5, 10: 2, 4.99: 20, 2.30: 5, 8.19: 30]
let stockSum = stock.reduce(0) {
  $0 + $1.key * Double($1.value)
}
let farmAnimals = ["🐎": 5, "🐄": 10, "🐑": 50, "🐶": 1]
let allAnimals = farmAnimals.reduce(into: []) {
  (result, this: (key: String, value: Int)) in
  for _ in 0 ..< this.value {
    result.append(this.key)
  }
}
let removeFirst = prices.dropFirst()
let removeFirstTwo = prices.dropFirst(2)
removeFirst = [10, 4.99, 2.30, 8.19]
removeFirstTwo = [4.99, 2.30, 8.19]
let removeLast = prices.dropLast()
let removeLastTwo = prices.dropLast(2)
removeLast = [1.5, 10, 4.99, 2.30]
removeLastTwo = [1.5, 10, 4.99]
let firstTwo = prices.prefix(2)
let lastTwo = prices.suffix(2)
firstTwo = [1.5, 10]
lastTwo = [2.30, 8.19]
prices.removeAll() { $0 > 2 } // prices is now [1.5]
prices.removeAll() // prices is now an empty array

Lazy Collections

Sometimes you can have a huge or even infinite collection, but you want to be able to access it somehow. A concrete example of this would be all of the prime numbers. That is an infinite set of numbers. So how can you work with that set? Enter the lazy collection. Consider that you might want to calculate the first ten prime numbers. To do this imperatively, you might do something like this:

func isPrime(_ number: Int) -> Bool {
  if number == 1 { return false }
  if number == 2 || number == 3 { return true }

  for i in 2...Int(Double(number).squareRoot()) {
    if number % i == 0 { return false }
  }

  return true
}

var primes: [Int] = []
var i = 1
while primes.count < 10 {
  if isPrime(i) {
    primes.append(i)
  }
  i += 1
}
primes.forEach { print($0) }
let primes = (1...).lazy
  .filter { isPrime($0) }
  .prefix(10)
primes.forEach { print($0) }

Mini-Exercises

  1. Create a constant array called names that contains some names as strings. Any names will do — make sure there are more than three. Now use reduce to create a string that is the concatenation of each name in the array.
  2. Using the same names array, first filter the array to contain only names longer than four characters and then create the same concatenation of names as in the above exercise. (Hint: You can chain these operations together.)
  3. Create a constant dictionary called namesAndAges containing some names as strings mapped to ages as integers. Now use filter to create a dictionary containing only people under the age of 18.
  4. Using the same namesAndAges dictionary, filter out the adults (those 18 or older) and then use map to convert to an array containing just the names (i.e., drop the ages).

Challenges

Before moving on, here are some challenges to test your knowledge of collection iterations with closures. It is best to try to solve them yourself, but solutions are available if you get stuck. Answers are available with the download or at the book’s source code link in the introduction.

Challenge 1: Repeating Yourself

Your first challenge is to write a function that will run a given closure a given number of times.

func repeatTask(times: Int, task: () -> Void)

Challenge 2: Closure Sums

In this challenge, you will write a function that you can reuse to create different mathematical sums.

func mathSum(length: Int, series: (Int) -> Int) -> Int

Challenge 3: Functional Ratings

In this final challenge, you will have a list of app names with associated ratings they’ve been given. Note — these are all fictional apps! Create the data dictionary like so:

let appRatings = [
  "Calendar Pro": [1, 5, 5, 4, 2, 1, 5, 4],
  "The Messenger": [5, 4, 2, 5, 4, 1, 1, 2],
  "Socialise": [2, 1, 2, 2, 1, 2, 4, 2]
]

Key Points

  • Closures are functions without names. They can be assigned to variables and passed as parameters to functions.
  • Closures have shorthand syntax that makes them easier to use than other functions.
  • A closure can capture the variables and constants from its surrounding context.
  • A closure can be used to direct how a collection is sorted.
  • A handy set of functions exists on collections that you can use to iterate over a collection and transform it. Transforms comprise mapping each element to a new value, filtering out certain values and reducing the collection down to a single value.
  • Lazy collections can be used to evaluate a collection only when strictly needed, which means you can efficiently work with large, expensive or potentially infinite collections.
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now