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].first!.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].first!.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.
- That classes aren’t the only reference types in Swift.
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(rank: .ace, suit: .hearts))
With multiple arguments:
hand.receive(
Card(rank: .two, suit: .spades),
Card(rank: .three, suit: .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(rank: .two, suit: .spades),
Card(rank: .three, suit: .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 in-out 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 Programming with Structures, 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.
Functions are reference types
In Function Types and Closures, you learned that functions have a type and that you can use these types to build higher-order functions. Here’s an example from that chapter:
func makeCounter() -> () -> Int {
var count = 0
return {
count += 1
return count
}
}
In this example, makeCounter
returns a closure that captures its local variable count
. This closure works like a counter. Every time you call it, it increments and returns its count
.
Every call to makeCounter
creates a new local variable and returns a new closure. This means all counters operate independently:
var c1 = makeCounter()
var c2 = makeCounter()
c1()
c2()
Now, consider the following example:
var c1 = makeCounter()
var c2 = makeCounter()
var c3 = c2
c1()
c2()
c3()
Unsurprisingly, the calls c1()
and c2()
return 1
. However, the call c3()
returns 2
. This is because functions are reference types.
c1
and c2
each hold a reference to a function, and the assignment var c3 = c2
copies the reference in c2
to c3
. The call c3()
returns 2
because it’s the second call to the function that both c2
and c3
refer to.
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?
}
class 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 property, method, or subscript also returns an optional, 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 value of an optional chain is still an optional. However, the chain does flatten multiple optionals into a single optional, so you have to unwrap only one optional, not three.
An optional chain can also be the target of an assignment:
authenticatedUser?.address?.province = "OVL"
This assignment only happens if neither authenticatedUser
nor address
are nil
.
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 fall back on a default value when the position is empty.
Implicitly unwrapped optionals
Earlier, you declared the following variable:
var authenticatedUser: User?
Now, suppose your application includes a screen where this user can configure their profile:
class UserProfileScreen {
private var user: User?
init() {
user = nil
}
func display(for user: User) {
self.user = user
// ...
}
func save(_ address: Address) {
user!.address = address
}
}
Most likely, you create an instance of this class when the application starts. However, at that point, the user hasn’t authenticated yet, so the user
property is nil
.
After the user authenticates, they can access the profile screen. When this happens, you call the display(for:)
method to set the user
property and display the screen.
In this example, user
isn’t really an optional. You set it when you call display(for:)
, and it never returns to nil
. All code that uses user
— such as save
— can safely assume it’s not nil
and force unwrap it. The only reason why user
is an optional is that its value isn’t available when you initialize the screen.
Implicitly unwrapped optionals support cases like this. You declare an implicitly unwrapped optional with an exclamation point (!
) instead of a question mark:
private var user: User!
As their name implies, the compiler implicitly unwraps these optionals for you:
func save(_ address: Address) {
user.address = address
}
Here, the compiler adds an implicit exclamation point after user
. The exclamation point in the declaration of user
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.
Types
This section continues the discussion on types from Structures, Enumerations, and Classes. 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.
- Enumeration cases can store additional information using 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:
class Player {
enum Rank {
case novice
case intermediate
case expert
}
// ...
}
Property observers
Consider the Image
class from Classes:
class Image {
var width: Int
var height: Int
private(set) var pixels: [Color]
// ...
}
A user of this class can set an image’s width and height, but not its pixels. It’s up to you, the developer of the class, 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.
Enumerations with associated values
Suppose that 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
.
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 the 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
Associated values are incompatible with raw values. A single enumeration cannot use both.
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)
)
)
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.