Chapters

Hide chapters

Swift Apprentice

1 min

Section III: Building Your Own Types

Section 3: 8 chapters
Show chapters Hide chapters

Section IV: Advanced Topics

Section 4: 13 chapters
Show chapters Hide chapters

8. Collection Iteration With Closures
Written by Matt Galloway

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

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 simply 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, then how do you use them?” To use a closure, you first have to 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 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 closure 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 some convenient features often associated with functional programming. These features come in the shape of functions that 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 collection, or perhaps even infinite, 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 in an imperative way 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

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. These came with the download or are available at the printed book’s source code link listed 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 a lot 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 easily 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.
© 2023 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now