NEW: Master Swift design patterns with my latest book! >>

How to loop over arrays

Paul Hudson    April 10th 2018    @twostraws

I know what you’re thinking: looping over arrays is trivial, so why should I read an article about it?

Well, it turns out there are several ways of looping over arrays and other collections, and it’s worth recapping them – hopefully you’ll find something new along the way.

First, let’s create a simple collection we can work with: a dictionary containing the names of various science fiction shows, plus a starships from each one:

let ships = [
    "Star Trek": "Enterprise",
    "Firefly": "Serenity",
    "Aliens": "Sulaco"
]

The standard, go-to approach to looping over that dictionary is like this:

for ship in ships {
    print("\(ship.value) is from \(ship.key)")
}

Because dictionaries are collections, just like arrays and sets, you can go over them in reverse:

for ship in ships.reversed() {
    print("\(ship.value) is from \(ship.key)")
}

And you can use enumerated() to get back their position and value in one go:

for (index, ship) in ships.enumerated() {
    print("\(index + 1). \(ship.value) is from \(ship.key)")
}

You can even combine enumerated() and reversed(), but be careful: enumerated().reversed() is not the same as reversed().enumerated():

// this will count down from 3 to 1
for (index, ship) in ships.enumerated().reversed() {
    print("\(index + 1). \(ship.value) is from \(ship.key)")
}

// this will count up from 1 to 3
for (index, ship) in ships.reversed().enumerated() {
    print("\(index + 1). \(ship.value) is from \(ship.key)")
}

Finally, you will occasionally see the forEach() method being used, which does much the same as a regular for-in loop except in a functional style:

ships.forEach {
    print("\($0.value) is from \($0.key)")
}

For forEach() method doesn’t do anything special. In fact, if you take a peek at the Swift source code you’ll see it literally just becomes a for-in loop:

public func forEach(_ body: (Element) throws -> Void) rethrows {
    for element in self {
        try body(element)
    }
}

That doesn’t mean it’s useless, though: using forEach() does guarantee your closure will be run on every item in the collection (there are no surprise break or return statements in there!).

Using forEach() is also nice when you want to call a function with each value. For example, you might write a function to calculate factors, like this:

func calculateFactors(_ number: Int) {
    let factors = (1...number).filter { number % $0 == 0 }
    let stringFactors = factors.map { String($0) }
    let joinedFactors = stringFactors.joined(separator: ", ")
    print("Factors of \(number): \(joinedFactors)")
}

You can then call that directly using forEach():

(1...100).forEach(calculateFactors)

What are you trying to achieve?

Previously I’ve talked about the importance of writing code that conveys your intent, which is why I often recommend map() and compactMap() when transforming array items.

The same lesson applies to looping: when you write a loop, it’s important to consider what you’re actually trying to achieve, because Swift can often provide alternatives that are clearer and perhaps even safer too.

The most common problem people face is looping over array indices, like this:

let movies = [
    "The Phantom Menace",
    "Attack of the Clones",
    "Revenge of the Sith",
    "A New Hope",
    "The Empire Strikes Back",
    "Return of the Jedi"
]

for i in 0 ..< movies.count {
    print("Episode \(i + 1): \(movies[i])")
}

That will print out the numbers and names of the first six Star Wars movies. Of course, any Star Wars fan will know how bad the Phantom Menace was, so they’d probably write this:

for i in 1 ..< movies.count {
    print("Episode \(i + 1): \(movies[i])")
}

And real Star Wars fans would write this:

for i in 3 ..< movies.count {
    print("Episode \(i + 1): \(movies[i])")
}

But both of those code examples contain a possible problem: if your movies array is empty, or lower than your start position, then your code will crash at runtime because creating a range where the start is higher than the end isn’t allowed.

The smarter thing to do is ditch using the index counter entirely by relying on dropFirst() instead. Called with no parameters this will drop the single first item, and called with a parameter will drop that many items.

For example:

for movie in movies.dropFirst() {
    print("Star Wars: \(movie)")
}

for movie in movies.dropFirst(3) {
    print("Star Wars: \(movie)")
}

If you call dropFirst() on an empty array or try to drop more items than you have, it won’t cause a crash – you’ll just get back an empty subsequence.

You can of course combine this with enumerated() or reversed(), like this:

for (index, movie) in movies.dropFirst(3).enumerated() {
    print("Episode \(index + 4): \(movie)")
}

What matters here is that your intent is clear: you want to loop over the array, skipping the first items.

And if you really do want to count from zero up to the size of an array, you should use the indices property instead:

for i in movies.indices {
    print("Episode \(i + 1): \(movies[i])")
}

Compare these two lines of code:

for i in 0 ..< movies.count {
for i in movies.indices {

I know the second option is a little shorter, but it also clarifies your intent – you’re not looping from one arbitrary value to another, but instead going over each index inside the array.

Again, this is so similar to map(). I often quote Javier Soto, who said “map() allows us to express what we want to achieve, rather than how this is implemented.” The same is true of indices: rather than explicitly creating a range by hand, we’re relying on Swift to figure that out.

As with the other approaches, indices can be used with reversed() if you want to loop backwards through a collection:

for i in movies.indices.reversed() {

That kind of code is so much easier to read than the equivalent where you specify the limits by hand:

for i in (0 ..< movies.count).reversed() {
    print(i)
}

As you’ve seen there are lots of ways you can loop over collections. You won’t use them all at the same time, but hopefully you’ve seen how each variant has its own uses.

 

About the author

Paul Hudson is the creator of Hacking with Swift, the most comprehensive series of Swift books in the world. He's also the editor of Swift Developer News, the maintainer of the Swift Knowledge Base, and Mario Kart world champion. OK, so that last part isn't true. If you're curious you can learn more here.

Click here to visit the Hacking with Swift store >>