This article is also available in Chinese.
One often-misused piece of the Swift standard library is Sequence’s enumerated()
function. This function give you a new sequence with each element in the original sequence and a number that corresponds to that element.
enumerated()
is misunderstood. Because it provides a number with each element, it can be an easy solution for a number of problems. However, most of those problems are solved better with other techniques. Let’s go over some of those cases, highlight why they’re flawed, and go over better abstractions for solving them.
The primary issue with using enumerated()
is that people think it returns each index and its element, but that’s not quite what’s going on. Because it’s available on every Sequence, and sequences aren’t even guaranteed to have indexes, we know they’re not indexes. Within the code, the variable is called offset
, not index
, which is another clue that we’re not dealing with indexes. The offset is always an integer, starting from 0, increasing by one for each element. For Array
, this happens to correspond to the index, but for basically every other type, it doesn’t. Let’s take a look at an example.
let array = ["a", "b", "c", "d", "e"]
let arraySlice = array[2..<5]
arraySlice[2] // => "c"
arraySlice.enumerated().first // => (0, "c")
arraySlice[0] // fatalError
Our variable arraySlice
here is, unsurprisingly, an ArraySlice
. However, its startIndex
is notably 2, not 0. But when we call enumerated()
and first
on it, it returns a tuple with an offset of 0 and its first element, “c”.
Put another way, you think you’re getting
zip(collection.indices, array)
but you’re actually getting
zip(0..<collection.count, array)
and this will result in wrong behavior anytime you’re working with anything other than an Array
.
Besides the fact fact you’re getting an offset rather than an index, there are other issues with using enumerated()
as well. If you’re tempted to use enumerated()
, there might be a better abstraction to take advantage of. Let’s take a look at a few.
The most common use I see of of enumerated()
is using the offset from one enumerated array to grab a corresponding element from of another array.
for (offset, model) in models.enumerated() {
let viewController = viewControllers[offset]
viewController.model = model
}
While this code works, it relies on both models
and viewControllers
being arrays, indexed by integers, starting from 0. It also relies on each array having the same length. If the models
array is shorter than the viewControllers
array, nothing bad will happen, but if the viewControllers
array is shorter than the models
array, we’ll have a crash. We also have this extra variable offset
that isn’t doing very much. A nicer, Swiftier way to write this same code is:
for (model, viewController) in zip(models, viewControllers) {
viewController.model = model
}
This code is much more focused and now works with any Sequence
types. It will also handle mismatched array lengths safely.
Let’s look at another example. This code adds an autolayout constraint between the first imageView
and its container, and then also between each pair of image views.
for (offset, imageView) in imageViews.enumerated() {
if offset == 0 {
imageView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor).isActive = true
} else {
let imageToAnchor = imageViews[offset - 1]
imageView.leadingAnchor.constraint(equalTo: imageToAnchor.trailingAnchor).isActive = true
}
}
This code sample has similar problems. We want consecutive pairs of elements, but using enumerated()
to get numbers that we can manipulate means we’re mucking about with indexes manually when we can we working at a higher level. zip
can help us here as well.
First, let’s write some code to deal with the container constraint on the first element:
imageViews.first?.leadingAnchor.constraint(equalTo: containerView.leadingAnchor).isActive = true
Next, lets handle the pairs:
for (left, right) in zip(imageViews, imageViews.dropFirst()) {
left.trailingAnchor.constraint(equalTo: right.leadingAnchor).isActive = true
}
Done. No indexes, works with any (multi-pass) Sequence, and more focused.
(You can also box up the pairing code into an extension and call .eachPair()
if you like.)
There are some valid uses of enumerated()
. Because what you’re getting is not an index, but just an integer, the right time to use it is when you need to work with a number (not an index) that corresponds to each element. For example, if you need to position a bunch of views vertically, where each one has y
coordinate that corresponds to some height multiplied by the offset in the sequence, enumerated()
is appropriate. Here’s a concrete example:
for (offset, view) in views.enumerated() {
view.frame.origin.y = offset * view.frame.height
}
Because the offset
here is being used for its properties as a number, enumerated()
works great.
The rule of thumb here is simple: if you’re using enumerated()
for an index, there might be a better way to solve your problem. If you’re using it for its properties as a number, thumbs up emoji.