Lesson 2 - Filtering and mapping arrays in Swift
In the previous lesson, Introduction to collections and genericity in Swift, we explained genericity in Swift.
We're getting to another important part of programming. As you already know from the lesson about arrays in Swift, they are often useful for storing multiple elements of the same type and working with them further. That's why collections exist.
Swift offers three primary collections which are quite straightforward:
Array
- we already know arraysDictionary
Set
Each collection comes in handy in different situations, but the most used one
is probably Array
.
Array
Swift implements its Array
as a modern collection and it's used
widely in this language. Just to jog your memory: we don't have to specify the
array size or care about it in any way, we are provided with simple methods for
adding items, removing them and several others. We already know the basic ones
so we're going to skip them. Today's lesson is going to be all about the
advanced array methods.
Let's create a new console application named e.g. Collections
and prepare a simple array of numbers in main.swift
:
var numbers = [2, 5, 9, 12, 34, 17, 28, 18]
filter()
We often need to "filter" the array and get only those elements which match a
given condition. We write this condition in a special block called
closure. All we need to know for now is that this block is
similar to a function which we can pass to a method as a parameter. Basically,
we pass a function to a function. It may sound a bit confusing but if you think
about it, it makes sense. For example, we call the filter()
method
and make it filter the array so it'll contain only the values matching our
closure (function). We'll now pass such a closure specifying how to filter our
number array to the filter()
method. If we start typing
numbers.filter
and let Xcode to generate the rest of the code,
we'll get something like this:
numbers.filter(isIncluded: (Int) throws -> Bool)
We declare closures using this arrow, so now we should be able to declare a method which would take one as an argument. But that's not what we want so we'll confirm by Enter again. Xcode will generate a blank closure. The result looks like this:
numbers.filter { (Int) -> Bool in
code
}
The (Int) -> Bool
expression means that we need to check
whether a number (Int
) should stay in the array or not
(Bool
). Int
in the parenthesis and "code" in the block
are highlighted. Now we'll write the variable name representing individual array
elements into the parenthesis where Int
is written at the moment,
so we can further work with the elements. Since we have numbers in our array,
let's name the variable number
:
numbers.filter { (number) -> Bool in
}
The filter()
method executes a closure for each number in the
array and depending on the returned Bool
value decides whether the
number should stay in the filtered array or not. The closure is still empty.
Let's say we want only numbers bigger than 10
, in that case the
block would look like this:
return number > 10
And the whole code like this:
numbers.filter { (number) -> Bool in return number > 10 }
If we print the result, we can see that the numbers really match the condition.
print(numbers)
Shortened notation
Maybe you're thinking that's a long code for just filtering an array, since
we're just comparing the number with the value of 10
. Swift,
fortunately, offers a fairly shorter notation, but we have to write it all by
ourselves.
numbers.filter { $0 > 10 }
That's much better, isn't it? $0
represents the variable which
we named number
before, and we don't need return
at
all. That's because it's clear that the expression is returned and so it's used
as the return value automatically. Note that the parentheses are no
longer there. This syntax is called trailing closure.
If the method had any standard parameters, we'd pass them in parentheses, close
it, and than pass the closure. It could look like this:
instance.method("someParameter") { $0 > 10 }
Now when we can filter arrays, let's have a look at the similar yet very
useful method, map()
.
map()
We use the method to transform data to a different data type or different
structure. All we need to do in the closure is to define how to
transform the existing data. For example, we'll want to multiply all
the numbers by two. First, let's see the code generated by Xcode. It should
remind you the one for the filter()
method:
numbers.map { (Int) -> T in
}
T
replaces the specific data type; it's genericity we explained
last time. Now all we need to do is to replace T
with the data type
we want to return. Because we just multiply, the return type stays
Int
. The whole code of calling the map()
method for
multiplying the elements by two would look like this:
numbers.map { (number) -> Int in return number * 2 }
Of course, we could shorten it using a trailing closure:
numbers.map { $0 * 2 }
Maybe you are thinking we could do all of this in some loop even without
knowing closures. However, that would take more effort, we'd have to declare a
new array and store the individual elements there, one by one. Besides, it's
really straightforward what we're trying to do using filter()
and
map()
when reading the code.
Multiplying numbers is of course a very basic example. A real-life scenario
would be, for example to use map()
if we had an array of
Student
objects and wanted to get an array of their email
addresses.
In the next lesson, Dictionaries in Swift, we'll discuss dictionaries which Swift implements
as the Dictionary
class.