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
orcontinue
a specific statement using statement labels.- Execute multiple
switch
cases using thefallthrough
statement.
Labeled statements
The following example shows some of the control flow statements in the doPlayerTurn
function from Blackjack:
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
:
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:
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:
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:
struct Hand {
private(set) var cards: [Card] = []
mutating func receive(_ cards: Card...) {
// ...
}
}
You can call this method with a single argument:
var hand = Hand()
hand.receive(Card(.ace, .hearts))
With multiple arguments:
hand.receive(
Card(.two, .spades),
Card(.three, .diamonds)
)
And even with zero arguments:
hand.receive()
A variadic parameter collects its arguments in an array. In the body of receive(_:)
, the type of cards
is [Card]
:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
func value(at position: Int) -> String {
if let player = positions[position] {
player.rawValue
} else {
"\(position + 1)"
}
}
You can also implement this method as follows:
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:
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:
var authenticatedUser: User!
As their name implies, the compiler implicitly unwraps these optionals for you:
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
:
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
:
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:
struct Player {
enum Rank {
case novice
case intermediate
case expert
}
// ...
}
Property observers
Consider the following types Color
and Image
:
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 asnewValue
. - A
didSet
observer runs immediately after the property has been set. This observer has access to the value that was replaced asoldValue
.
Here’s how a willSet
observer can adjust the image when the user sets the height
property:
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:
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:
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:
var credential: Credential
credential = .social(id: 123)
Case social
is meaningless without an associated id
:
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:
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:
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:
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
:
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:
var comment: String? = nil
comment = "enumerations are powerful"
The compiler transforms this code as follows:
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:
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:
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.