Skip to content

Wrapping Up

This chapter wraps up some loose ends and completes the topics for this course. These topics relate to earlier chapters but were postponed because they were too advanced or distracted from the main discussion of the chapter. Nevertheless, the topics are important enough to deserve a place in this course, which is why they’re included here.

Control flow

This section continues the discussion on control flow from Control Flow and Booleans and Advanced Control Flow. In this section, you’ll learn how to:

  • break or continue a specific statement using statement labels.
  • Execute multiple switch cases using the fallthrough statement.

Labeled statements

The following example shows some of the control flow statements in the doPlayerTurn function from Blackjack:

swift
repeat {  // for all hands
  if currentHand > 0 {
    if playerHands[currentHand][0].rank == "A" {
      continue  // with the next hand
    }
  }
  while value(for: playerHands[currentHand]).value < 21 {
    if answer == "s" {
      break  // no more actions for this hand
    }
    if answer == "p" {
      if playerHands[currentHand][0].rank == "A" {
        break  // no more actions for this hand
      }
    }
    if answer == "d" {
      break  // no more actions for this hand
    }
  }
} while currentHand < playerHands.count && !playerHands[currentHand].isEmpty

This code has a while loop nested in a repeat-while loop. Both loops contain nested if statements and use break or continue for additional control flow.

Unfortunately, this nested control flow obscures the relationship between the break and continue statements and the loops they affect.

You can improve this code by labeling the loop statements. With labels, you can be specific about which statement you want to break or continue:

swift
hands: repeat {
  if currentHand > 0 {
    if playerHands[currentHand][0].rank == "A" {
      continue hands
    }
  }
  actions: while value(for: playerHands[currentHand]).value < 21 {
    if answer == "s" {
      break actions
    }
    if answer == "p" {
      if playerHands[currentHand][0].rank == "A" {
        break actions
      }
    }
    if answer == "d" {
      break actions
    }
  }
} while currentHand < playerHands.count && !playerHands[currentHand].isEmpty

Labels act as a form of documentation and improve the clarity of your code. They also enable otherwise impossible control flow, such as breaking a loop from inside a nested loop or switch statement:

swift
enum Input {

  case play
  case quit
}

func readInput() -> Input {
  // ...
}

menu: while true {
  switch readInput() {
  case .play:
    // ...
  case .quit:
    break menu
  }
}

fallthrough

The fallthrough statement causes control to “fall through” from one switch case into the next. It ignores the next case’s condition and executes it as if it was part of the current case.

The following example uses fallthrough to include the benefits of a bronze membership in a silver membership, and those of a silver membership in a gold membership:

swift
enum Benefit {
  
  case priorityBoarding
  case seatSelection
  case discount
  case firstClass
}

enum Membership {
  
  case gold
  case silver
  case bronze
  
  var benefits: [Benefit] {
    var result: [Benefit] = []
    switch self {
    case .gold:
      result += [.firstClass]
      fallthrough
    case .silver:
      result += [.discount]
      fallthrough
    case .bronze:
      result += [.priorityBoarding, .seatSelection]
    }
    return result
  }
}

Functions

This section continues the discussion on functions from Functions and Function Types and Closures. In this section, you’ll learn:

  • How you can pass any number of arguments to a function using a variadic parameter.
  • How a function can mutate its arguments using in-out parameters.
  • How you can improve encapsulation by nesting functions.

Variadic parameters

A variadic parameter accepts any number of arguments of a single type. To declare a variadic parameter, you follow its type with three dots (...).

In the following example, the receive(_:) method has a variadic parameter that accepts any number of cards:

swift
struct Hand {
  
  private(set) var cards: [Card] = []
  
  mutating func receive(_ cards: Card...) {
    // ...
  }
}

You can call this method with a single argument:

swift
var hand = Hand()
hand.receive(Card(.ace, .hearts))

With multiple arguments:

swift
hand.receive(
  Card(.two, .spades),
  Card(.three, .diamonds)
)

And even with zero arguments:

swift
hand.receive()

A variadic parameter collects its arguments in an array. In the body of receive(_:), the type of cards is [Card]:

swift
mutating func receive(_ cards: Card...) {
  for card in cards {
    self.cards.append(card)
  }
}

A variadic parameter is useful when you don’t know ahead of time how many arguments a caller will need to pass. Without a variadic parameter, you’d have to use an array and wrap the arguments in square brackets:

swift
hand.receive([
  Card(.two, .spades),
  Card(.three, .diamonds)
])

A variadic parameter is more convenient because it creates the array for you. However, if you want to pass an existing array to a function, you need an array parameter:

swift
mutating func receive(_ cards: [Card]) {
  self.cards.append(contentsOf: cards)
}

In-out parameters

As you already know from Functions, function parameters are constants. When you call a function, you assign arguments to the function’s parameters, which are constants in the function’s body.

In-out parameters are different. An in-out parameter is a variable in the function body. You can change its value, and when the function ends, it copies the value of the parameter back to its argument, which must also be a variable.

For example:

swift
func increment(_ number: inout Int) {
  number += 1
}

var x = 0
increment(&x)

In this example, number is an in-out parameter, as indicated by the keyword inout before its type. The ampersand (&) before the argument x is required and reminds you that x is being passed in-out, and that the function will mutate its value. When increment returns, it assigns the incremented value of number back to x.

In-out parameters can improve the performance of your code. When you use an in-out parameter, the compiler can configure this parameter as a reference to its argument. This removes the overhead of copying large values, such as arrays, into and out of a function.

The Standard Library includes a reduce method that uses an in-out parameter for just this reason. Here’s how you use this method to remove the duplicates from an array of integers:

swift
var values = [1, 2, 3, 1]
values = values.reduce(into: []) {
  (result: inout [Int], next: Int) in
  if !result.contains(next) {
    result.append(next)
  }
}

This reduction starts with an empty array. The trailing closure processes an element of the original array and appends it to result if it’s not a duplicate. Because result is an in-out parameter, reduce can pass the same array to every reduction step, without having to create a new copy every time.

Nested functions

In Refactoring, you learned that you can reduce the complexity of your code by using the private keyword to hide a type’s internals from other types.

Another way you can improve encapsulation is by nesting functions. You can declare a function inside the body of another function. This nested function is only usable by its outer function and hidden from other functions.

As an example, consider the print method from the solution to Enumerations:

swift
func print() {
  Swift.print("""
    \(value(at: 0)) | \(value(at: 1)) | \(value(at: 2))
    -----------
    \(value(at: 3)) | \(value(at: 4)) | \(value(at: 5))
    -----------
    \(value(at: 6)) | \(value(at: 7)) | \(value(at: 8))
    """)
}

This method depends on another method, value(at:), which returns either the raw value for the player at a given position, or a default value:

swift
private func value(at position: Int) -> String {
  if let player = positions[position] {
    player.rawValue
  } else {
    "\(position + 1)"
  }
}

You only use value(at:) in print, so you can make it a nested method:

swift
func print() {
  func value(at position: Int) -> String {
    if let player = positions[position] {
      player.rawValue
    } else {
      "\(position + 1)"
    }
  }

  Swift.print("""
    \(value(at: 0)) | \(value(at: 1)) | \(value(at: 2))
    -----------
    \(value(at: 3)) | \(value(at: 4)) | \(value(at: 5))
    -----------
    \(value(at: 6)) | \(value(at: 7)) | \(value(at: 8))
    """)
}

This clearly conveys the purpose of value(at:) as a helper method for print and hides it from other functions.

Optionals

This section continues the discussion on Optionals. In this section, you’ll learn:

  • How you can access properties, methods, and subscripts of an optional value using optional chaining.
  • That some optionals are implicitly unwrapped when you use them.

Optional chaining

Consider the following example:

swift
struct Address {
  
  var province: String?
}

struct User {
  
  var address: Address?
}

var authenticatedUser: User?

In this example, there may be an authenticated user, who may have specified their address, which may include a province. This means you have to unwrap three optionals to find the user’s province:

swift
if let user = authenticatedUser {
  if let address = user.address {
    if let province = address.province {
      // ...
    }
  }
}

This construction of nested if statements is known as a pyramid of doom.

Optional chaining can save you from this doom. It’s a quick and safe way to access a property, method, or subscript of an optional value.

Here’s how you find the user’s province with an optional chain:

swift
if let province = authenticatedUser?.address?.province {
  // ...
}

You start an optional chain by adding a question mark (?) after an optional value. You can then access a property, method, or subscript of that optional as if it were unwrapped. If the return value is an optional as well, you can add another question mark and continue the chain.

An optional chain returns either the expected value — in this case, the province — or nil if any optional in the chain was nil. This means the type of authenticatedUser?.address?.province is String?.

As you can see, optional chaining doesn’t fully unwrap an optional. The result of an optional chain is still an optional. However, the chain does flatten multiple optionals into one, so you only have to unwrap the result of the chain, not every optional in it.

An optional chain can also be the target of an assignment:

swift
authenticatedUser?.address?.province = "OVL"

This assignment happens only if neither authenticatedUser nor address are nil. Otherwise, the statement is ignored.

Another example where optional chaining is useful is the value(at:) method from before:

swift
func value(at position: Int) -> String {
  if let player = positions[position] {
    player.rawValue
  } else {
    "\(position + 1)"
  }
}

You can also implement this method as follows:

swift
func value(at position: Int) -> String {
  positions[position]?.rawValue ?? "\(position + 1)"
}

This code combines optional chaining with the nil-coalescing operator (??) to safely read the rawValue property and provide a default value when the position is empty.

Implicitly unwrapped optionals

Earlier, you declared the following variable:

swift
var authenticatedUser: User?

Now, suppose your application requires authentication. This means that, other than during the authentication process, this variable will never be nil.

In that case, authenticatedUser isn’t really an optional. You set it when the user authenticates, and it never returns to nil. All code that uses this variable can safely assume it’s not nil and force unwrap it. The only reason why authenticatedUser is an optional is that its value may not be available when you initialize the application.

Implicitly unwrapped optionals support cases like this. You declare an implicitly unwrapped optional with an exclamation point (!) instead of a question mark:

swift
var authenticatedUser: User!

As their name implies, the compiler implicitly unwraps these optionals for you:

swift
func save(_ address: Address) {
  authenticatedUser.address = address
}

Here, the compiler adds an implicit exclamation point after authenticatedUser. The exclamation point in its declaration reminds you of this behavior. It also reminds you that implicit force unwraps are just as dangerous as explicit ones.

Implicitly unwrapped optionals should only be nil during initialization, never when you use them. In all other cases, you should use a regular optional and unwrap it safely.

Note

Swift will only unwrap an implicitly unwrapped optional when it needs a non-optional value. Otherwise, it treats implicitly unwrapped optionals just like regular optionals.

Types

This section continues the discussion on types from Structures, Enumerations, and Refactoring. In this section, you’ll learn that:

  • You can nest types to improve encapsulation and avoid name conflicts.
  • You can react to changes in properties using property observers.
  • Enumerations can store additional information as associated values.
  • Optionals are enumerations with associated values.
  • Associated values can lead to recursive enumerations.

Nested types

In Enumerations, you declared types Rank and Suit to represent the rank and suit of a card. You can make the relationship between these types explicit by nesting Rank and Suit inside Card:

swift
struct Card {

  enum Rank {
    // ...
  }

  enum Suit {
    // ...
  }

  // ...
}

Unlike functions, nested types aren’t hidden by default. Outside of Card, you refer to Rank and Suit as Card.Rank and Card.Suit. However, you can make these types private, in which case they’re only usable by Card.

Nested types also avoid name conflicts. Suppose that you want to create a type to represent a player’s experience level, and that you also want to name it Rank:

swift
enum Rank {

  case novice
  case intermediate
  case expert
}

Without nesting, the two Rank types will conflict, and the compiler will refuse your code. By nesting the types so that they become Card.Rank and Player.Rank, you can resolve this name conflict:

swift
struct Player {

  enum Rank {

    case novice
    case intermediate
    case expert
  }

  // ...
}

Property observers

Consider the following types Color and Image:

swift
struct Color {
  
  let red: Int
  let green: Int
  let blue: Int
  
  static let black = Color(red: 0, green: 0, blue: 0)
}

struct Image {
  
  var width: Int
  var height: Int
  private(set) var pixels: [Color]
  
  init(width: Int, height: Int) {
    self.width = width
    self.height = height
    pixels = Array(repeating: .black, count: width * height)
  }
}

A user of type Image can set an image’s width and height, but not its pixels. It’s up to you, the developer of the type, to adjust the image when the user resizes it. For this, you can use property observers.

A property observer is a block of code that you attach to a property. The observer executes whenever the property is mutated.

Property observers come in two flavors:

  • A willSet observer runs immediately before the property is set. This observer has access to the value being set as newValue.
  • A didSet observer runs immediately after the property has been set. This observer has access to the value that was replaced as oldValue.

Here’s how a willSet observer can adjust the image when the user sets the height property:

swift
var height: Int {
  willSet {
    if newValue > height {
      let newRows = newValue - height
      pixels += Array(repeating: .black, count: width * newRows)
    } else {
      let deletedRows = height - newValue
      pixels.removeLast(width * deletedRows)
    }
  }
}

This observer either adds rows of black pixels or crops the image. You can achieve the same result with a didSet observer:

swift
var height: Int {
  didSet {
    if height > oldValue {
      let newRows = height - oldValue
      pixels += Array(repeating: .black, count: width * newRows)
    } else {
      let deletedRows = oldValue - height
      pixels.removeLast(width * deletedRows)
    }
  }
}

Observers aren’t active during initialization. You only use them to react to changes that happen after the instance has been initialized.

Associated values

Suppose you’re building an application that offers two authentication options: users either provide an email address and password credential, or they use a social network account. You can use an enumeration to model these options:

swift
enum Credential {
  
  case custom(email: String, password: String)
  case social(id: Int)
}

This enumeration has associated values that store additional information with each case. As you can see, each case can have a different set of zero or more associated values.

You must specify associated values when you instantiate a case that has them:

swift
var credential: Credential
credential = .social(id: 123)

Case social is meaningless without an associated id:

swift
credential = .social  

This is because social no longer represents a single value. Instead, it represents a category of values — one for each possible id.

Note

Associated values are incompatible with raw values. A single enumeration cannot use both.

Reading associated values

Associated values aren’t properties. Although each instance stores its associated values, you cannot access them using dot syntax, nor can you change them.

You must use a value-binding pattern to read associated values:

swift
switch credential {
case .custom(let email, let password):
  print("Authenticated as \(email) with password \(password).")
case .social(let id):
  print("Authenticated as user #\(id).")
}

This pattern isn’t exclusive to the switch statement. Here’s how you use it with an if statement:

swift
if case .social(let id) = credential {
  print("Authenticated as user #\(id).")
}

Note the additional case keyword. This keyword brings the pattern-matching abilities of the switch statement to other statements. It also differentiates patterns from Boolean conditions and optional bindings.

Conforming to Equatable and Hashable

Unlike regular enumerations, those with associated values do not automatically conform to Equatable and Hashable. However, the compiler can synthesize implementations for you:

swift
extension Credential: Equatable { }
extension Credential: Hashable { }

This feature requires that the associated values are themselves Equatable or Hashable.

Optionals are enumerations

Swift implements optionals as enumerations with associated values. The Standard Library includes the following type Optional:

swift
enum Optional<Wrapped> {

  case some(Wrapped)
  case none
}

This simple enumeration powers all optionals. It states that an optional type wraps an existing type, and that optional values either contain some value of this wrapped type, or no value at all.

Note

The angle brackets in this declaration are part of a feature called generics that you’ll learn about in a future course. You’ve seen this syntax before in Arrays and Dictionaries.

Because optionals are so prevalent, the compiler provides plenty of syntactic sugar to make them easier to use. Consider the following example:

swift
var comment: String? = nil
comment = "enumerations are powerful"

The compiler transforms this code as follows:

swift
var comment: Optional<String> = .none
comment = .some("enumerations are powerful")

As you can see, type String? is equivalent to Optional<String>, the keyword nil is equivalent to case .none, and assigned values are automatically wrapped in the .some case.

Syntactic sugar makes powerful features easier to use, and Swift provides plenty of it. Use it whenever possible, but take some time to explore what goes on behind the scenes, as this will greatly improve your understanding of the language you’re using.

Recursive enumerations

The type of an associated value can be any type, even the enumeration itself. This results in a recursive enumeration.

Here’s a recursive enumeration that models simple algebraic expressions:

swift
indirect enum Expression {
  
  case constant(Double)
  case addition(Expression, Expression)
  case subtraction(Expression, Expression)
  case multiplication(Expression, Expression)
  case division(Expression, Expression)

  var value: Double {
    switch self {
    case .constant(let value):
      value
    case let .addition(lhs, rhs):
      lhs.value + rhs.value
    case let .subtraction(lhs, rhs):
      lhs.value - rhs.value
    case let .multiplication(lhs, rhs):
      lhs.value * rhs.value
    case let .division(lhs, rhs):
      lhs.value / rhs.value
    }
  }
}

The keyword indirect marks this enumeration as recursive and is required to avoid an infinite loop. You add this keyword either to the enumeration as a whole or to the recursive cases only.

You can use type Expression to implement a simple calculator. The user can input an expression such as 1 + 2 * 3, and you can evaluate this expression as follows:

swift
let expr = Expression.addition(
  .constant(1),
  .multiplication(
    .constant(2),
    .constant(3)
  )
)
print(expr.value)

Up next

This chapter completes the topics for this course. Up next is your final challenge, where you’ll be able to apply everything you’ve learned so far.