An in-depth look at for loops in Swift

Published on: October 16, 2019

In Swift, there is more than one way to loop over a list of elements. If we only consider the loops that aren't intended to create new lists or have side effects, you end up with two types of loops that are very similar; forEach and for item in list, or the for loop. In this week's Quick Tip, I will explain the difference between for and forEach loops, and you I will show some examples that should help you make a decision picking one of either loop methods, depending on your needs.

Understanding for loops

Commonly when you write code that need to do something with every item in a list (Array, Set or other Sequence), you might write something like the following:

let list = [1, 2, 3, 4, 5]

for item in list {
  // do something with item
}

This is possibly the most basic example of a for loop and I'm sure you have written loops like this at least once before you found this Quick Tip. So, what else can we do with for loops?

A cool feature that for loops have is that you can apply a filter to them using a where clause. This means that your loop body is only called for elements that meet the requirements specified in the where clause. The following code is an example of such a filter:

let list = [1, 2, 3, 4, 5]

for item in list where item > 2 {
  print(item)
}

The above code applies a where clause to the for loop that makes sure any element that we receive in the for loop's body is larger than two. This can make your for loop more readable than checking this criterion inside of the for loop body. This segues nicely into learning about the continue keyword that can be used in a for loop!

When you write continue in a for loop, you are telling Swift to stop executing the for loop body for the current item and move over to the next item in the list. The following code produces the same output as the for loop that has a where clause:

let list = [1, 2, 3, 4, 5]

for item in list {
  guard item > 2
    else { continue }

  print(item)
}

In terms of performance, you won't really notice a difference between using a guard or a where clause. You might think that the where clause would make sure you don't loop over as many items but this is not entirely true. Swift will still need to make sure whether each item meets the criteria specified in the where clause. The bigger difference in my personal opinion is readability and semantics. The version that has a where clause tells me exactly under what condition a certain item is passed on to the for loop's body, but it also makes it very easy to spot that there is a condition present in the first place. In the example with the guard, you need to spend a little bit more time looking at the code to figure out that we need the item to be larger than two. Of course, a where clause isn't always the best option, especially if there are multiple, potentially complicated, reasons for an item to be considered skippable.

Sometimes, you don't want to loop over every item in a list. You might want to only loop until you have found a certain item, much like Swift's first(where:) method does. Or maybe you just want to collect items from an array of numbers until you've reached a certain accumulated value. An example of this is shown in the following code snippet:

let list = [1, 2, 3, 4, 5]
var accumulated = 0

for item in list {
  accumulated += item

  if accumulated >= 4 {
    break
  }
}
print(accumulated)

This code loops through items and adds them to an accumulated value. Once this accumulated value is larger than or equal to 4, the break keyword is used to break out of the for loop. Doing this abandons the loop altogether, unlike continue which will only cancel the current executing of the for loop body, allowing the next item to be processed. In the example above, we would loop over 1, then 2 and finally 3. This puts the accumulated value at 6, and we will never reach the 4 or 5 in the list since the loop is cancelled using the break keyword.

Another way to break out of a for loop early is by returning a value:

let list = [1, 2, 3, 4, 5]

func findFirstItemLargerThan(_ threshold: Int) -> Int? {
  for item in list {
    if item > threshold {
      return item
    }
  }

  return nil
}

print(findFirstItemLargerThan(3))

Returning a value from a for loop will prevent further invocations of the loop body and immediately returns the provided value to the caller of the method that contains the for loop. This can be useful if you're implementing a function that should find the first match in a list, like in the example above.

Two other important features of for loops are the ability to use named for loops, and the ability to nest for loops. Let's look at both of these features in a single example:

let list = [1, 2, 3, 4, 5]

outerLoop: for item in list {
  for nestedItem in list {
    if item == nestedItem {
      continue outerLoop
    }

    print(item, nestedItem)
  }
}

By prepending the outer for loop with the outerForLoop: label, we can now refer to it from inside of the inner loop. In the preceding code example, we loop over a list of integers in the outer loop. We loop over the same list in the inner loop. If we would omit the if statement and continue statement you would see an output that roughly looks like this:

1 1
1 2
1 3
1 4
1 5
2 1

//etc...

With the continue outerLoop statement in place, you see the following output:

2 1
3 1
3 2
4 1

//etc...

If you try removing the outerLoop label from the continue statement, you'll find that only the entries where item and nestedItem are equal will not be printed since the inner loop would move on to the next item in the inner list. By putting the outerLoop after the continue statement, the outer loop itself will proceed to the next item in the outer list. Pretty powerful stuff!

In this short introduction to for loops, you have seen several features and ways to use for loops. However, I promised you a comparison between for and forEach, so let's have a look at what a forEach loop is, and how it's different from a for loop.

Comparing for loops to forEach loops

Honestly, it's almost unfair to compare for loops to forEach loops, because if you look at how forEach is defined in the Swift language you might quickly understand how they're different:

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

We can see that forEach is a method that takes a closure and uses a for loop to iterate over all of the elements in the list and invokes the closure with each element as the argument. Let's look at a simple usage example:

let list = [1, 2, 3, 4, 5]

list.forEach { item in
  print(item)
}

So, what are the differences between for and forEach then? If they both are really just a for loop, they must be pretty much the same right? Unfortunately, they are not the same. A forEach loop lacks the capability to apply a where clause, use a continue statement, you can't return anything from a forEach loop and you also can't break out of it. You can nest forEach loops but you can't label them and you can't access (or continue / break / return from) the outer loop.

A forEach will always iterate over all elements in your list and apply whatever closure you pass it to every single element in your list, no exceptions.

In summary

In this blog post, you learned a lot about for loops, and you saw how Swift implements the forEach method. Now that you understand the differences between the two different ways of looping, you should be able to determine which loop you need. Do you need to break out of the loop early? Or maybe you want to use a where clause to prevent the loop body from being called for every item. Or maybe you're just looking for the first item to match a certain condition and you want to return that item. In all of these scenarios you'll want to use a for loop or a built-in function like first(where:) from the Swift standard library.

Alternatively, if you want to loop over every single item in an array or another type of list, without any exceptions, you can safely use forEach since that's what it's good at.

Thanks for reading this blog post and as always, your input, feedback, and questions are highly appreciated. Reach out on Twitter if you want to get in touch!

Subscribe to my newsletter