Control Flow
Control flow are the language constructs
that allow to change the behavior of a program depending on its data.
Swift uses if
, switch
or guard
to represent conditional statements,
and for-in
, while
or repeat-while
to represent loops.
if
statements
An if
statement allows the program to check for a condition:
let pokemonLevel = 38
if pokemonLevel > 30 {
print("the Pokemon won't obey")
} else {
print("the Pokemon will obey")
}
Parentheses around the conditions are optional, and should not be used unless they make the code clearer.
Unlike most languages of the C-family, Swift conditions must be a Boolean expression (i.e. of type
Bool
). As a result,if pokemonLevel { ... }
is not be a valid expression in the above example, and the compiler would complain about it.
A special use of if
is to both check if an optional variable is set (non-nil
) and assign its value to another variable.
It is written as below:
let pokemonLevel: Int? = 38
if let level = pokemonLevel {
print("the Pokemon level is \(level)")
} else {
print("the Pokemon level is unknown")
}
Note that the above example is equivalent, but arguably clearer, than the following code. You should use the previous variant as it is idiomatic of the Swift language.
if pokemonLevel != nil {
print("the Pokemon level is \(pokemonLevel!)")
} else {
print("the Pokemon level is unknown")
}
Another way to handle optional variables is use the infix operator ??
.
It returns its left part if it has a value, and is thus a non-optional or a non-nil
optional;
and retuns its right part in the other cases.
This operator is used mainly to provide a default value when using optional variables:
let level = pokemonLevel ?? 1
When testing an enumeration variable,
it is possible to test if it is an instance of a specific case
,
and to extract the associated properties.
You should use this construct as it is idiomatic of the Swift language:
indirect enum SpeciesType {
// ...
case dual(primary: SpeciesType, secondary: SpeciesType)
}
let lotadType = SpeciesType.dual(primary: .water, secondary: .grass)
if case .dual(primary: let primary, secondary: let secondary) = lotadType {
print("Lotad has two types: \(primary) and \(secondary)")
}
// "Lotad has two types: water and grass"
This feature is a simplified case of pattern matching. It is able to match a variety of patterns, that are presented later in this section, for instance:
let pokemon = (number: 001, name: "Bulbasaur")
if case let (number: x, name: y) = pokemon {
print("\(y) has number \(x)")
}
if case 0 ... 10 = pokemon.number {
print("the pokemon number is comprised between 0 and 10")
}
In addition to matching and extracting parts of a value, pattern matching can also apply some conditions. Conditions are given separated by commas, that represent an “and”:
if case let (number: x, name: y) = pokemon, x > 50 {
print("\(y) has a number greater than 50")
}
if case let x = pokemon.number, x > 50 {
print("the pokemon number is greater than 50")
}
Ternary Operator _?_:_
The ternary operator is a equivalent to an if
in an expression.
If its first operand is true, it returns its second operand, and returns its third one otherwise:
typealias Species = (number: Int, name: String)
let pokemon = (number: 001, name: "Bulbasaur")
let another = pokemon.number == 001 ? pokemon : (number: 002, name: "Ivysaur")
This operator should be used only when necessary. Programmers should prefer to use pattern matching instead.
switch
statements
A switch
statement is a generalization of if
.
It allows to select one among several conditions.
Contrary to the switch statement found in many languages,
the switch
in Swift performs pattern matching.
Pattern matching can, for instance, check that a number is within a range.
The switch
statement works as follows:
it tests each case
in order, from the top to the bottom,
and executes the first one that matches.
If no case
matches the input, the default
part is executed.
In the following example, note that cases are over range of integers.
The second case 30 .. 100
overlaps with the first one,
but with no problem as it is tested after:
let pokemonLevel = 31
switch pokemonLevel {
case 50 ... 100:
print("the Pokemon won't obey unless we have 4 badges")
case 30 ... 100:
print("the Pokemon won't obey unless we have 2 badges")
default:
print("the Pokemon will obey")
}
// "the Pokemon won't obey unless we have 2 badges"
A switch
statement must cover all possible cases.
If Swift detects missing cases, it gives an error at compile-time.
let pokemonLevel = 31
switch pokemonLevel {
case 50 ... 100:
print("the Pokemon won't obey unless we have 4 badges")
case 30 ... 100:
print("the Pokemon won't obey unless we have 2 badges")
case 0 ... 100:
print("the Pokemon will obey")
}
// error: switch must be exhaustive, consider adding a default clause
The programmer should give a default
clause if it is difficult to describe all cases.
For instance, as the pokemonLevel
is a integer,
the previous switch
is complete with respect to the knowledge of the programmer (levels are between 0 and 100),
but incomplete for the Swift compiler (integers are between larger bounds).
The programmer should add a default
clause with an assertion:
let pokemonLevel = 31
switch pokemonLevel {
case 50 ... 100:
print("the Pokemon won't obey unless we have 4 badges")
case 30 ... 100:
print("the Pokemon won't obey unless we have 2 badges")
case 0 ... 100:
print("the Pokemon will obey")
default:
assert (false)
}
Unlike most C-family languages, Swift does not require a break
after each case block.
Instead, only the code explicitly written in the matched case is executed,
and the switch
statement transfers control as soon as it’s finished.
If multiple cases are handled by the same code, they are separated by a comma:
let pokemonLevel = 4
switch pokemonLevel {
case 2, 4, 6:
print("the Pokemon level is 2, 4 or 6")
default:
break
}
// "the Pokemon level is 2, 4 or 6"
This tutorial has already shown cases for ranges, enumerations and tuples.
All these patterns
are available within switch
.
More information on patterns in available in this blog post.
typealias Species = (number: Int, name: String)
struct Pokemon {
let species: Species
var level: Int
}
let sparky = Pokemon(species: (number: 135, name: "Jolteon"), level: 31)
switch sparky {
case let pokemon where pokemon.species.name == "Jolteon":
print("the Pokemon is a Jolteon with level \(pokemon.level)")
case let pokemon where pokemon.level > 50:
print("the Pokemon won't obey unless we have 4 badges")
default:
break
}
// "the Pokemon is a Jolteon"
indirect enum SpeciesType {
// ...
case dual(primary: SpeciesType, secondary: SpeciesType)
}
let lotadType = SpeciesType.dual(primary: .water, secondary: .grass)
switch lotadType {
case .dual(primary: let primary, secondary: let secondary):
print("the Pokemon has 2 types: \(primary) and \(secondary)")
default:
print("the Pokemon has 1 type: \(lotadType)")
}
// "the Pokemon has 2 types: water and grass"
Unlike in pattern matching with
if-statement
, checking additional constraints on matched variables requires the use of the keywordwhere
, rather than separating them with a comma:if case let (x, y) = someType, x > y { /* ... */ } switch someTuple { case let (x, y) where x > y: /* ... */ default: break }
The reason is that the comma also serves as a separator for multiple cases.
for-in
loops
A for-in
loop iterates over a sequence of elements:
var speciesNames = ["Bulbasaur", "Charmander", "Squirtle"]
for i in 0 ... 2 {
print(speciesNames[i])
}
// Bulbasaur
// Charmander
// Squirtle
Notice the
0 ... 2
in the above example. It creates a closed range from 0 to 2 included. Swift also has another range operator,..<
, which creates half-open ranges. That is0 ..< 2
creates a range from 0 to 2 but where 2 isn’t included.
The for-in
loop can iterate over anything that is a sequence.
For instance, a character string is also a sequence of Character
:
for character in "ヒトカゲ".characters {
print(character)
}
// ヒ
// ト
// カ
// ゲ
Arrays, sets and dictionaries are also sequences.
Hence they can be iterated over with a for-in
loop.
Iteration over arrays returns each element of the array:
typealias Species = (number: Int, name: String)
let species: [Species] = [(001, "Bulbasaur"), (004, "Charmander"), (007, "Squirtle")]
for oneSpecies in species {
print(oneSpecies.name)
}
// Bulbasaur
// Charmander
// Squirtle
❔
What was the advantage of explicitly typing the array in the above example?
Iteration over sets is similar to iteration over arrays. It returns each element of the set:
let speciesNames: Set = ["Bulbasaur", "Charmander", "Squirtle", "Bulbasaur"]
for speciesName in speciesNames {
print(speciesName)
}
// Bulbasaur
// Charmander
// Squirtle
Iteration over dictionaries is a bit different, as dictionaries are key-value pairs.
It thus returns pairs containing a key and its associated value (that is non nil
):
indirect enum SpeciesType { /* ... */ }
let speciesTypes = ["Bulbasaur": SpeciesType.grass, "Charmander": SpeciesType.fire]
for (speciesName, speciesType) in speciesTypes {
print("species \(speciesName) has type \(speciesType)")
}
// species Charmander has type fire
// species Bulbasaur has type grass
The for-in
construct supports pattern matching, like an if
.
Programmers can use any pattern that has been presented previously:
let speciesTypes = ["Bulbasaur": SpeciesType.grass, "Charmander": SpeciesType.fire]
for case let (name, _) in speciesTypes where name.hasSuffix("aur") {
print(name)
}
// Bulbasaur
while
and repeat-while
loops
A while
loop repeats a block of code as long as its condition holds.
The loop can thus never be executed:
let evolutions = ["Bulbasaur": "Ivysaur", "Ivysaur": "Venusaur"]
var pokemon = (level: 1, species: "Bulbasaur")
while evolutions[pokemon.species] != nil {
pokemon = (level: pokemon.level, species: evolutions[pokemon.species]!)
}
// pokemon: (level: Int, species: String) = {
// level = 1
// species = "Venusaur"
// }
A repeat-while
loop words similarly, but checks the condition after the block is executed,
rather than before.
The loop is thus executed at least once:
let evolutions = ["Bulbasaur": "Ivysaur", "Ivysaur": "Venusaur"]
var pokemon = (level: 1, species: "Bulbasaur")
repeat {
pokemon = (level: pokemon.level, species: evolutions[pokemon.species]!)
} while evolutions[pokemon.species] != nil
// pokemon: (level: Int, species: String) = {
// level = 1
// species = "Venusaur"
// }
❔
What happens if the list of
evolutions
is empty in the two previous loops?
The keyword continue
skips the remainder of loop body.
It is used to force passing to the next iteration.
In the case of a for-in
or while
loop, the program will pass to the next iteration and then evaluate the condition,
whereas in the case of repeat-while
loop, the program will evaluate the condition and then pass to the next iteration.
let evolutions = ["Bulbasaur": "Ivysaur", "Ivysaur": "Venusaur"]
var pokemon = (level: 1, species: "Bulbasaur")
repeat {
if pokemon.species == "Ivysaur" {
continue
}
pokemon = (level: pokemon.level, species: evolutions[pokemon.species]!)
} while evolutions[pokemon.species] != nil
// infinite loop, because the condition is always `true`
The keyword break
ends the loop immediately.
let evolutions = ["Bulbasaur": "Ivysaur", "Ivysaur": "Venusaur"]
var pokemon = (level: 1, species: "Bulbasaur")
repeat {
if pokemon.species == "Ivysaur" {
break
}
pokemon = (level: pokemon.level, species: evolutions[pokemon.species]!)
} while evolutions[pokemon.species] != nil
// pokemon: (level: Int, species: String) = {
// level = 1
// species = "Ivysaur"
// }
The continue
and break
statements respectively skip and end the loop within which they are defined.
When nesting loops, it is sometimes desirable to perform those operation on an outer loop.
In order to do that, it is possible to label the loops, so that continue
and break
can specify on which loop they should be applied:
enum SpeciesType { case grass, fire, water }
struct Pokemon {
let species: (number: Int, name: String)
var level: Int
}
let speciesTypes = ["Bulbasaur": SpeciesType.grass, "Charmander": SpeciesType.fire]
let pokemons = [
Pokemon(species: (number: 001, name: "Bulbasaur"), level: 01),
Pokemon(species: (number: 004, name: "Charmander"), level: 01)
]
var result : Pokemon? = nil
outer: for pokemon in pokemons {
inner: for (name, speciesType) in speciesTypes {
if (name == pokemon.species.name) && (speciesType == .grass) {
result = pokemon
break outer
}
}
}
result
// $R0: Pokemon? = some {
// species = {
// number = 1
// name = "Bulbasaur"
// }
// level = 1
// }
guard
statements
A guard
statement is similar to an if
statement, but is preferred in situations where some condition must hold for the program flow to continue:
struct Pokemon { /* ... */ }
let bulby = Pokemon(species: (number: 001, name: "Bulbasaur"), level: 8)
switch bulby {
case let pokemon where pokemon.species.name == "Bulbasaur":
guard pokemon.level >= 16 else {
print("the Pokemon cannot evolve yet")
break
}
print("the Pokemon can evolve")
default:
break
}
// the Pokemon cannot evolve yet
Notice the use of the
break
keyword in theelse
clause of the guard. This is becauseguard
should always transfer control if the condition doesn’t hold, usingbreak
,continue
or other kind of statements we’ll see later.
A guard
statement can always be replaced with an if
statement.
Programmers should use if
when both the “then” and “else” parts contain parts of the algorithm.
On the contrary, guard
should be used when the algorithm only continues for the “then” part.