For loops are a fundamental construct of programming, and even more so of making iOS apps. If you want to become an iOS developer, they are a necessary tool you need to have in your toolbox.
But more important than simply understanding how they work, is to know how to use them for the different tasks you will meet in iOS development.
So, the aim of this article is not only to show you how Swift for loops work, but how to use them to solve the most common programming problems.
- This article is part of the Learn Swift series.
FREE GUIDE - SwiftUI App Architecture: Design Patterns and Best Practices
MVC, MVVM, and MV in SwiftUI, plus practical lessons on code encapsulation and data flow.
DOWNLOAD THE FREE GUIDEContents
- Using for loops to iterate over the content of collections like arrays, dictionaries, and strings
- Iterating over numbers and the indexes of the elements in a sequence, and specifying increments
- Iterating over the sorted elements of sequence, reversing their order and decrementing numbers
- Ignoring elements and accumulating partial results in an external variable
- Skipping iterations and interrupting a loop before it gets to the end
- Replacing for loops with map, filter and reduce
Using for loops to iterate over the content of collections like arrays, dictionaries, and strings
The most common use of for loops in the Swift programming language is to iterate over the contents of a collection. And the most common collection in Swift programming are arrays, which are ordered collections of elements.
The instructions we put in the body of a for loop are repeated for any element in an Array
.
For example:
let names = ["John", "Anna", "Jordan", "Fred", "Stephanie"]
for name in names {
print("Hello \(name)!")
}
// Hello John!
// Hello Anna!
// Hello Jordan!
// Hello Fred!
// Hello Stephanie!
Swift for loops though can iterate over any type that conforms to the Sequence protocol.
The second most used collection in Swift programming is the dictionary (also known as a hash map in other languages).
Dictionaries can be considered sequences of key-value pairs, so we can iterate over their content as well.
let releaseDates = ["Mac": 1984, "iPod": 2001, "iPhone": 2007, "iPad": 2010, "Apple Watch": 2015]
for (product, year) in releaseDates {
print("The \(product) was first released in \(year)")
}
// The iPhone was first released in 2007
// The Mac was first released in 1984
// The Apple Watch was first released in 2015
// The iPod was first released in 2001
// The iPad was first released in 2010
Be aware that the contents of a Dictionary
are unordered. When you iterate over a dictionary, no order is guaranteed, not even the order in which you inserted the elements into the collection.
Finally, in Swift, strings are also sequences, so we can loop over their letters as we do for any other collection.
for letter in "Hello" {
print(letter)
}
// H
// e
// l
// l
// o
Iterating over numbers and the indexes of the elements in a sequence, and specifying increments
Languages like C and Java have a for loop constructs that allow you to iterate over numbers. In those, you specify a starting number, an ending one, and an increment.
Swift had that feature too, but it was removed in Swift 3.
Nowadays, Swift for loops only work on sequences. If you need to iterate over numbers, you need to use a numeric range, which is a type that conforms to the Sequence
protocol.
Use a half-open range if you want to exclude the last number, or a closed range to include it.
// Half-open range
for number in 3..<7 {
print(number)
}
// 3
// 4
// 5
// 6
// Closed range
for number in 3...7 {
print(number)
}
// 3
// 4
// 5
// 6
// 7
If you want to increment values by more than one at each step, you have to use the stride(from:to:by:)
or stride(from:through:by:)
functions. The former excludes the last number in the stride, while the latter include it.
for number in stride(from: 3, to: 11, by: 2) {
print(number)
}
// 3
// 5
// 7
// 9
for number in stride(from: 3, through: 11, by: 2) {
print(number)
}
// 3
// 5
// 7
// 9
// 11
Finally, for loops are often used to get the index of an element in a sequence.
For this task, other languages use the loop construct that the Swift language abandoned. In Swift, we instead use the enumerated()
function, which returns a tuple containing both the index and the element.
let beverages = ["Coffee", "Tea", "Chocolate"]
for (index, beverage) in beverages.enumerated() {
print("\(beverage) is our warm drink number \(index + 1)")
}
// Coffee is our warm drink number 1
// Tea is our warm drink number 2
// Chocolate is our warm drink number 3
Iterating over the sorted elements of sequence, reversing their order and decrementing numbers
We don’t always iterate on the elements of a sequence in their original order.
A common need is to iterate over the sorted elements of a collection. That’s easily done by calling the sorted()
method on the sequence in a for loop.
let seasons = ["Spring", "Summer", "Autumn", "Winter"]
for season in seasons.sorted() {
print(season)
}
// Autumn
// Spring
// Summer
// Winter
The sorted()
function does not affect the original array, but it returns a new one instead.
We can infer this behavior from the function’s name. According to the Swift API Design Guidelines, functions that use the -ed or -ing forms of a verb return a new, independent value. Functions with side effects, instead, use imperative verbs, like sort()
.
Sorting is often useful with dictionaries. As mentioned above, iterating over a dictionary does not guarantee any order.
The Dictionary
type offers a sorted(by:)
method, but you need to pass a function to it specifying the ordering.
That’s often superfluous. When the keys in a dictionary conform to the Comparable protocol, you can instead iterate over them and retrieve the corresponding value from the dictionary.
Swift types like Int
, String
, and others conform to Comparable
, so this approach works most of the times.
let ages = ["John": 21, "Anna": 14, "Jordan": 11, "Fred": 30, "Stephanie": 40]
for name in ages.keys.sorted() {
print("\(name) is \(ages[name]!) years old")
}
// Anna is 14 years old
// Fred is 30 years old
// John is 21 years old
// Jordan is 11 years old
// Stephanie is 40 years old
Finally, to iterate over the elements of a sequence in reverse order, you can use the reversed()
method.
let seasons = ["Spring", "Summer", "Autumn", "Winter"]
for season in seasons.reversed() {
print(season)
}
// Winter
// Autumn
// Summer
// Spring
Remember that ranges and strides are collections too, so you can sort or reverse those as well.
for number in (0...4).reversed() {
print(number)
}
// 4
// 3
// 2
// 1
// 0
Ignoring elements and accumulating partial results in an external variable
Often, you want to ignore all or some elements in a sequence while you iterate over them.
If all you want to do is repeat an operation a certain number of times, but you don’t care about that number in the body of the for loop, you can use the _
character.
for _ in 1...3 {
print("Hip hip hooray!")
}
// Hip hip hooray!
// Hip hip hooray!
// Hip hip hooray!
Using the _
character is necessary because when you give a name to the variable, the Swift compiler complains that you are not using it anywhere. This tells the compiler that we did it on purpose.
Sometimes, we want to skip some of the elements in a sequence.
It’s possible, of course, to use an if statement in a for loop for that. For example, we can print all even numbers between 1 and 10:
for number in 1...10 {
if number.isMultiple(of: 2) {
print(number)
}
}
// 2
// 4
// 6
// 8
// 10
But Swift for loops also allow us to use pattern matching. Instead of using an if statement inside the body, we can add a where
clause to the loop, specifying any boolean condition to filter the elements of a sequence.
for number in 1...10 where number.isMultiple(of: 2) {
print(number)
}
// 2
// 4
// 6
// 8
// 10
The elements that do not satisfy the where
clause never appear in the body of the loop, so we don’t have to consider them.
Iterating over the elements of a sequence does not always mean doing something with each item separately. Sometimes, you need to combine them into a single result.
For that, you need to accumulate the partial result in a variable declared outside of the for loop, which you update at every iteration.
For example, to add some numbers together, you would write:
var sum = 0
for number in 1...10 {
sum += number
}
print(sum)
// 55
The initial value of the external variable depends, of course, on the operation you perform. For a multiplication, for example, you would start from 1 instead of 0.
If you want to instead transform each element in a sequence, you can append each result to the end of an array.
var squares: [Int] = []
for number in 1...5 {
squares.append(number * number)
}
print(squares)
// [1, 4, 9, 16, 25]
Skipping iterations and interrupting a loop before it gets to the end
There are cases in which you want to skip a cycle in a for loop if some condition is true.
This is different from using a where
clause, which filters out all elements for which the predicate is false.
There are cases in which we might want our loop body to execute for every element and then decide whether to do something or skip to the next cycle.
In those cases, we use the continue
keyword.
For example, let’s check if a given number is prime. An integer is prime only if it’s divisible by one and by itself.
A rudimentary way to check if any number is prime is to go through all the integers that precede it and check if any of these is a divisor.
let number = 27
var isPrime = true
for divisor in 2..<number {
if !number.isMultiple(of: divisor) {
continue
}
isPrime = false
}
print("\(number) \(isPrime ? "is" : "is not") prime")
// 27 is not prime
In the code above, we cycle through the possible divisors of a number. At every cycle, we check if our number is a multiple of the divisor. If not, we skip to the next cycle using continue
.
Let’s now say that we want to find all the primes up to the number 10.000. We can place the above loop inside another one that goes over all numbers. When a number is prime, we add it to an array.
for number in 3...10_000 {
var isPrime = true
for divisor in 2..<number {
if !number.isMultiple(of: divisor) {
continue
}
isPrime = false
}
if isPrime {
primes.append(number)
}
}
The problem of any loop with a large number of iterations is that running it takes a ton of time. The above takes 34 seconds to run in an Xcode playground (on my MacBook Pro from mid-2015).
Admittedly, this is not the best integer factorization algorithm. Still, we can improve it significantly with a simple addition.
The main problem is that even when we discover that a number is not prime, the inner for loop goes until the end. You can see in the image above that the isPrime = false
statement runs 73.669 times.
That’s a massive waste of time, especially since half of the numbers are even, so we know already that they are not prime.
What we want to do is to stop the inner loop as soon as we find out that a number is not prime. For that, we use the break
keyword.
With just one extra line of code, we got a massive improvement in performance.
Replacing for loops with map, filter and reduce
Swift offers some useful alternatives to for loops: functional operators.
In functional programming, loops do not exist. Iteration is instead mainly done using three functions called map, filter and reduce, which, internally, use recursion to iterate over a sequence.
Let’s first see how these three functions work, and then we will see why they are useful.
Their basic idea is pretty simple when you compare it to the examples we saw above.
The filter(_:)
function removes the elements in a sequence that don’t satisfy a predicate. It is equivalent to a for loop with a where
clause.
For example, we can rewrite our example above to get the even numbers between 1 and 10 using filter(_:)
.
let evenNumbers = (1...10).filter { number in
number.isMultiple(of: 2)
}
// [2, 4, 6, 8, 10]
There are a few significant differences to notice about map, filter, and reduce:
- They use closures. These might look like the body of a for loop, but they are effectively full-fledged functions.
- They return a result. In a for loop, you need to instead explicitly accumulate your partial results in an external variable at every cycle.
The map(_:)
function transforms every element in a sequence. It is equivalent to a for loop that appends the result of every iteration to an array.
For example, we can rewrite our example above to calculate the square of numbers between 1 and 5 using map(_:)
.
let squares = (1...5).map { number in
number * number
}
// [1, 4, 9, 16, 25]
Finally, the reduce(_:)
function combines the results of a loop in a single value. It is equivalent of a for loop that accumulates the result of each cycle in an external variable.
For example, we can rewrite our example above that sums the numbers between 1 and 10 using reduce(_:)
.
let sum = (1...10).reduce(0) { result, number in
result + number
}
// 55
Reduce has the most complex syntax of the three functions. It takes the initial number for the computation, and then a function that combines the current value with the partial result of all previous iterations.
Since map, filter, and reduce seem to recreate functionality we already have, you might wonder why they exist. They might be necessary for functional programming, but in Swift, we have loops.
First of all, you might have already noticed that the using map, filter, and reduce is slightly shorter than the equivalent for loops. But it’s when we combine them that they become much more powerful.
For example, let’s say that we want to do all three operations listed above: take the even numbers between 1 and 10, calculate their squares and then add them all.
When using for loops, we need to accumulate the intermediate results of each loop in separate variable so that the next loop can use them.
var evenNumbers: [Int] = []
for number in 1...10 where number.isMultiple(of: 2) {
evenNumbers.append(number)
}
var squares: [Int] = []
for number in evenNumbers {
squares.append(number * number)
}
var sum = 0
for number in squares {
sum += number
}
// 220
Map, filter, and reduce return a result, so we don’t need these intermediate variables. Instead, we can chain the three functions and get the final result in a much more compact way.
let sum = (1...10)
.filter { number in number.isMultiple(of: 2) }
.map { number in number * number }
.reduce(0) { result, number in result + number }
// 220
If you are interested in the functional approach, you can read my article on Swift functional programming.
Conclusions
Iterating over sequences, especially over arrays and dictionaries, is a fundamental part of building iOS apps.
I created the examples in this article to show you how to use for loops in Swift for the most common tasks. For didactical reasons, I had to keep them simple, so they might seem a bit sterile and an end unto themselves.
SwiftUI App Architecture: Design Patterns and Best Practices
It's easy to make an app by throwing some code together. But without best practices and robust architecture, you soon end up with unmanageable spaghetti code. In this guide I'll show you how to properly structure SwiftUI apps.
Matteo has been developing apps for iOS since 2008. He has been teaching iOS development best practices to hundreds of students since 2015 and he is the developer of Vulcan, a macOS app to generate SwiftUI code. Before that he was a freelance iOS developer for small and big clients, including TomTom, Squla, Siilo, and Layar. Matteo got a master’s degree in computer science and computational logic at the University of Turin. In his spare time he dances and teaches tango.