Arrays
enable you to group and order elements of the same type, so they play a crucial role in organizing your app’s data. If you’re building an iOS app, arrays are a vital part of your toolkit, and today we’re going to help you understand them.
Specifically, we will give you a backend view of how Arrays work and jump into a host of specific operations, from simple filtering and sorting to complex ways of mapping and reducing them. We’ll finish with tips on how to leverage Business Logic in your own apps.
Table of Contents
Understanding arrays in Swift
We have already introduced Swift arrays in our article about Swift collections, in this article you can find how to perform the basic operation on Swift arrays: Creating a Swift array, accessing array elements, iterating an array, etc. Now, this is an advanced article that will provide more an in-depth understanding of Swift arrays and what you can achieve with them.
To really understand and master Arrays
in Swift, we should start by defining them.
Arrays
, in Swift, are a data structure or Swift collection that can hold multiple elements. These data structures that are value types. This means that whenever we assign an Array
to a variable, or pass it via a function, what we’re doing is assigning an individual copy. This is in contrast to reference Swift data types, whereby a single copy is used throughout.
To explain this distinction, Swift’s own developer docs imagine a person sharing a document, so their colleagues can make changes.
- Creating a value type is like downloading a copy and sharing it by email. The recipient can make changes, but this won’t affect the original.
- Creating a reference type is like sharing a link to the original document in the Cloud. The recipient can make changes, and these will directly affect the original.
Now let’s look at these definitions and explore the contingent coding concepts in more detail.
Value-based nature of arrays
Copying semantics When you assign an array to another variable or pass it as a function parameter, a new copy of the Array
is created. This ensures that modifications to the resulting array do not affect the original array.
var originalArray = [1, 2, 3]
var copiedArray = originalArray
copiedArray[0] = 99
print(originalArray)
// Output: [1, 2, 3]
print(copiedArray)
// Output: [99, 2, 3]
Immutability Swift’s value-based nature allows you to create immutable arrays (arrays which, once created, cannot be changed) using the let
keyword. Once an array is declared as a constant, its contents cannot be modified.
let constantArray = [4, 5, 6]
constantArray[0] = 44
// This would not compile at all, since constantArray is a constant
Comparison to reference-based types
Defining what a concept isn’t can often help us understand what it is. A definition of its alternative can help us identify its own attributes and characteristics.
So let’s examine how Arrays would work if they were reference-based. Specifically let’s look at what happens when we change the properties of a class, which, in Swift, is a reference type:
class Person {
var name: String
init(name: String) {
self.name = name
}
}
var person1 = Person(name: "Alice")
var person2 = person1
person2.name = "Bob"
print(person1.name)
// Output: Bob
print(person2.name)
// Output: Bob
In the example above, modifying person2
also modifies the underlying object. This change is reflected in person1
, because they both refer to the same instance of the Person
class.
Value types vs reference types: Pros and cons
Both value types and reference types present distinct advantages to developers, and their application will depend on your specific project requirements.
Advantages of value types
- Predictable behavior. Changes to a value type do not affect other instances.
- Thread safety. Value types are inherently thread-safe as each instance is independent.
Advantages of reference types
- Shared state. Reference types allow multiple references to share the same underlying instance and value.
- Reduced memory overhead: Copies of reference types share the same underlying data.
Understanding the distinction between value and reference types is very helpful if we want to get a full grasp of what Arrays can do.
Advanced Swift arrays operations
Ok, now, we’ve looked at the theory behind Arrays, let’s start looking at practical examples of what they can do, we will see how to sort an array, using map & reduce and more. But first we need to understand a new Swift concept.
Understanding higher-order functions
Most of the operations that we will look at today will rely on higher-order functions. So, before we start, let’s take a quick overview of the concept.
The term higher-order functions, or HoFs, comes from functional programming, and it’s just a way of saying that Swift functions are treated as first-class citizens in our paradigm. This means that the function, to qualify for HoF status, has to achieve at least one of the following:
- Accept a function as a parameter.
- Return a function as its result.
Ok, we know what you’re thinking: a couple of practical examples could be really useful here. So here are a couple.
// Function 1
func factorialOf(number: Int) -> Int {
var aux = 1
repeat {
aux = aux * number
number -= 1;
} while(number > 1)
return aux
}
// Function 2
func factorialCalculator() -> ((Int)->(Int)) {
return { input in
var aux = 1
repeat {
aux = aux * input
input -= 1;
} while(input > 1)
return aux
}
}
Function number 1 isn’t a HoF, since it does not accept or return a Function
.
Function
number 2 does qualify for HoF status since it does return a Function
, which itself calculates a factorial.
Right, now on to the specific functions.
Sorting a Swift array
Sorting is a crucial part of Array management. As we said at the top, the whole point of Arrays is to order our elements, so we need a clear, consistent way of doing this.
Well actually there are two principal methods we can use for sorting:
sort()
sorts an Array in place.sorted()
gives us a sorted copy of theArray
, while leaving the original unchanged.
To simplify and reduce repetition, we’ll use sorted
in our examples from here on, but you can always replace it with sort
in any scenario that fits your needs better.
There are lots of specific techniques we can use to sort Arrays, now we’re going to look at the generic sort
/sorted()
.
let numberArray = [1, 2, 10, 15, 3, 4, 5]
print(numberArray.sorted())
//Prints [1, 2, 3, 4, 5, 10, 15]
By default, this sorts the array in ascending order. If we want to descend, we can simply provide the “bigger than” operation using a Swift closure function as follows:
print(numberArray.sorted(by: { number1, number2 in
number1 > number2
}))
//Prints [15, 10, 5, 4, 3, 2, 1]
//This can also be shortened to:
print(numberArray.sorted(by: >))
But this throws up an obvious question: What if we have custom objects instead of just numbers?
In this case we have two options, and we’ll demonstrate them to you with the following struct (which, like an array, is a value type).
struct Person {
var firstName: String
var lastName: String
var age: Int
}
extension Person {
static func makeRandom() -> Person {
let firstNames = ["John", "Teresa", "Leonard", "Penny", "Raphael", "Marie"]
let lastNames = ["Cena", "Bombadil", "Staedler", "Philips"]
return Person(firstName: firstNames[Int.random(in: 0...firstNames.count - 1)],
lastName: lastNames[Int.random(in: 0...lastNames.count - 1)],
age: Int.random(in: 1...100))
}
}
//That's our Person class with a way to make random test people for our article
//Let's create an array of people:
var peopleArray: [Person] = []
repeat {
peopleArray.append(Person.makeRandom())
} while (peopleArray.count < 50)
//Now if our goal is to order them from younger to older, or vice-versa, there's 2 options:
//Option 1 - Make the Person Comparable so it can be used on a regular sort/sorted:
extension Person: Comparable {
static func < (lhs: Person, rhs: Person) -> Bool {
return lhs.age < rhs.age
}
}
//Now we can easily use
print(peopleArray.sorted())
//And it will sort our array perfectly by age
//Option 2 - If we only need it in one place,
//or if we do not want to make Comparable, we can always compare it in the sort function:
print(peopleArray.sorted(by: {
person1, person2 in
return person1.age > person2.age
})
)
The previous Swift code snippet shows two ways to sort a Swift array of custom Person objects based on their age.
Sorting with Swift Comparable protocol
- The Person struct is defined with firstName, lastName, and age properties.
- An extension is added to Person to implement the Swift Comparable protocol. This is done by defining the < operator, which compares Person objects based on their age.
- With Person conforming to Comparable, the sorted() method can be used directly on an array of Person objects to sort them.
Sorting using Swift closures functions
- Alternatively, without making Person conform to Comparable, a custom closure can be used with the sorted(by:) method.
- The closure { person1, person2 in return person1.age > person2.age } sorts the Person objects in descending order of age.
In this case, comparting persons by age was simple as age was stored as an Integer, usually this will be stored by saving the person birth date, in this case you might need to compare the two Swift dates using Swift date operations.
Filter an array
To demonstrate filtering, another method in our list of higher order functions, we’re just going to go straight into examples, showing Arrays
of people:
let people = [
Person(firstName: "Alice", lastName: "Brown", age: 25),
Person(firstName: "Bob", lastName: "Smith", age: 30),
Person(firstName: "Charlie", lastName: "Brown", age: 22),
Person(firstName: "David", lastName: "Trump", age: 35)
]
//Let's filter anyone older than 30 out:
let youngPeople = people.filter { $0.age < 30 }
//Now let's filter for only people named "Brown"
let namedBrown = people.filter { $0.lastName == "Brown" }
//We can also create blocks that are filters, and afterwards use them whenever we'd like:
let isNamedBrownFilter: (Person) -> Bool = { $0.lastName == "Brown" }
let namedBrownUsingANamePredicate = people.filter(isNamedBrownFilter)
Transforming a Swift array with map
There are three types of Maps
methods in Swift arrays: the Map
, the flatMap
, and the compactMap
. All of them are higher-order functions and they’re all commonplace in Swift.
We’ll look at each of them in detail, so you get a clear idea of how and when to use each one.
The map
Maps
are used to transform the elements of an Array
into different objects. They accept a transformative function as their argument and they return the transformed argument.
To take a very simple example, let’s map an Array
of Ints
to an Array
of Strings
:
let arrayOfInts = [1, 2, 3, 4, 5]
let arrayOfStrings = numbers.map {
$0.description
}
However maps have a limitation: they need to provide the same number of elements in the outputs as in the inputs. This means that, by design, we can’t filter nil elements from them.
To illustrate this for you, let’s look at a struct detailing a group of people and cars:
//This is our House Struct.
//Each House as a mandatory address, and optionally inhabitants
struct House {
var address: String
var inhabitants: [Person]?
}
//This is our Car struct, that only has owners
struct Car {
let owners: [Person]
//Notice that we have a failable initialiser
//So if there's no owners, there's no car
init?(owners: [Person]?) {
if owners == nil {
return nil
}
self.owners = owners ?? []
}
}
//Now we'll try mapping from an House to a car
let cars = arrayOfHouses.map {
return Car(owners: $0?.inhabitants)
}
print(cars)
//This will print:
/*
[
Optional(Car(owners: [
Person(firstName: "Marie", lastName: "Staedler", age: 44),
Person(firstName: "Teresa", lastName: "Cena", age: 98)])),
Optional(Car(owners: [
Person(firstName: "Leonard", lastName: "Staedler", age: 53),
Person(firstName: "John", lastName: "Bombadil", age: 38)])),
nil
]
*/
We still have three elements, as we had initially, but the mapped data includes one element which is nil
. When we need to filter out these nil values, another form of mapping comes into play.
The compactMap
The compactMap
is very similar to a regular Map
. The biggest difference is that the compactMap
automatically filters out nil
values. If we used the exact same example as before, but only changed the form of map
, this is what we’d get:
//...
// Same Classes as above
//Now we'll try mapping from an House to a car
let cars = arrayOfHouses.compactMap {
return Car(owners: $0?.inhabitants)
}
print(cars)
//This will print:
/*
[
Optional(Car(owners: [
Person(firstName: "Marie", lastName: "Staedler", age: 44),
Person(firstName: "Teresa", lastName: "Cena", age: 98)])),
Optional(Car(owners: [
Person(firstName: "Leonard", lastName: "Staedler", age: 53),
Person(firstName: "John", lastName: "Bombadil", age: 38)
])),
]
*/
This time, when printing, we only get 2 cars
and no nil
values. This is very helpful in cases where we do not want to have any nils
after our transformation.
The flatMap
The third, and last kind of map, is the flatMap
. Like the others, it’s an higher-order function that will transform our Array
. The difference between flatMap
, and map
, is that flatMap
will flatten the result into one single Array
.
By way of example, let’s reuse our Car Class
from earlier and try mapping the inhabitants of the houses into an Array
of Arrays
of inhabitants:
let arrayOfHouses: [House?] = [
House(address: "Random Street 1", inhabitants: [
Person.makeRandom(),
Person.makeRandom()
]),
House(address: "Random Street 4", inhabitants: [
Person.makeRandom(),
Person.makeRandom()
]),
]
let inhabitants = arrayOfHouses.map {
return $0?.inhabitants
}
//This will print an array of arrays:
/*
[
[
Person(firstName: "Raphael", lastName: "Staedler", age: 87),
Person(firstName: "Raphael", lastName: "Staedler", age: 13)
],
[
Person(firstName: "Leonard", lastName: "Staedler", age: 49),
Person(firstName: "Raphael", lastName: "Bombadil", age: 37)
]
]
*/
That’s an array of Arrays
of inhabitants, but what would happen if we flatMapped
it instead?
// ...
//same declarations as above
let inhabitants = arrayOfHouses.flatMap {
return $0?.inhabitants
}
//This will print an array:
/*
[
Person(firstName: "Raphael", lastName: "Staedler", age: 87),
Person(firstName: "Raphael", lastName: "Staedler", age: 13)
Person(firstName: "Leonard", lastName: "Staedler", age: 49),
Person(firstName: "Raphael", lastName: "Bombadil", age: 37)
]
*/
This joins the inner Arrays
of the original Map
in one single Array
, removing any nested Collections
or multidimensional arrays.
Swift array reduce
Reduce
is the final higher-order function we’ll cover today. Once you’ve mastered this one you’ll be able to handle any conceivable operation using Arrays
in Swift.
Reduce
aims to combine all elements in an Array
into one single value. Not one single Array
like the flatMap
, but one single value. This function is usually used to calculate numbers from a given array. To use it, we provide the initial value, and then again on the closure it uses. This gives us access to the current result, and the next value too. We use this data to calculate what the result should be.
Now let’s look at examples of how the reduce function can be used:
//A simple example where we just add up the numbers in an Integer Array
let numbers = [1, 2, 3, 4, 5]
let sum = numbers.reduce(0) { (result, next) in
return result + next
}
// sum is now 15 (0 + 1 + 2 + 3 + 4 + 5)
//Another example in which we start from 5, and then
//we multiply the numbers by double the next one
let multiplication = numbers.reduce(5) { result, next in
return result * (next * 2)
}
// multiplication now has the value of 9600
//Now a more realistic scenario that we could find in a real world app:
//We have an house, with a number of inhabitants, and we want to sum up their ages
let house = House(address: "Random Street 1", inhabitants: [
Person.makeRandom(),
Person.makeRandom(),
Person.makeRandom(),
Person.makeRandom()
])
let ageSum = house.inhabitants?.reduce(0, { partialResult, nextPerson in
return partialResult + nextPerson.age
})
//Now lets complicate it a bit:
//We have an Array of Houses, each house has inhabitants,
//and we need to sum all the inhabitants ages
let arrayOfHouses: [House?] = [
House(address: "Random Street 1", inhabitants: [
Person.makeRandom(),
Person.makeRandom(),
Person.makeRandom(),
Person.makeRandom()
]),
House(address: "Random Street 4", inhabitants: [
Person.makeRandom(),
Person.makeRandom(),
Person.makeRandom(),
Person.makeRandom()
]),
]
let sumOfAges = arrayOfHouses.reduce(0) {
partialResult, next in
return partialResult + next.inhabitants.reduce(0, {
inhabitantsPartialResult, nextPerson in
return inhabitantsPartialResult + nextPerson.age
})
}
Beautiful bit of coding, isn’t it?
This last one is a bit more complex, but we’re reducing the ages of each house’s inhabitants to a single value, and then adding all those values at once.
We can take an even closer look by using a for-cycle
to do the same exact thing. Let’s take a look:
var sumOfAges = 0
arrayOfHouses.forEach {
house in
house?.inhabitants?.forEach({ person in
sumOfAges += person.age
})
}
Swift array reduce
performance
Are there any reasons why we should use reduce
instead of regular for-loops
?
Well, one of them is performance. While we shouldn’t prematurely optimise our code, higher-order functions are much more suitable for these kinds of operations.
Now let’s add further code to help us measure the performance:
let startTime = CFAbsoluteTimeGetCurrent()
let sumOfAges = arrayOfHouses.reduce(0) { partialResult, next in
return partialResult + next.inhabitants.reduce(0, { partialResult, nextPerson in
return partialResult + nextPerson.age
})
}
let elapsed = CFAbsoluteTimeGetCurrent() - startTime
print("elapsed: \\(elapsed)")
//Printed 3.993511199951172e-05, which we can convert to 39,99 Microseconds
let startTime = CFAbsoluteTimeGetCurrent()
var sumOfAges = 0
arrayOfHouses.forEach {
house in
house?.inhabitants?.forEach({ person in
sumOfAges2 += person.age
})
}
let elapsed2 = CFAbsoluteTimeGetCurrent() - startTime
print("elapsed: \\(elapsed)")
//Printed 8.499622344970703e-0, which we can convert to 89,99 Microseconds
While we’re talking about a very minimal unit, Microseconds, the performance of reduce
is twice as good as that of for-cycles
, which is impressive in itself.
In this tutorial we are writing some advanced Swift code that can be hard to verify later, we recommend you to read our article about Swift Unit testing so you can write tests to make sure everything works as expected and doesn’t break in the future.
Swift array extensions: extend arrays to fit our needs
Extending Arrays
is just like extending any other Class
in Swift. To demonstrate this, let’s look at another example.
We’re going to make an extension to fix one issue that Arrays
have by default: not having a safe way of accessing an array element in a specific index position without checking the index first:
//One of the ways to safely access one Array's element is by checking the index
guard myArray.indices.contains(myIndex) else {
//error path where we leave scope
}
//But wouldn't it be cool if we could directly,
//in a Swifty way safely access the element?
guard let myElement = myArray[myIndex] else {
//error path where we leave scope
}
//Unfortunately, the above, will just crash our app if the index does not exist
//Let us fix it then. Somehwere in your app, just add:
extension Array {
/// Returns the element at the specified index if it is within bounds, otherwise nil.
subscript (safe index: Index) -> Element? {
return indices.contains(index) ? self[index] : nil
}
}
//Now, anywhere in your app, when accessing an element of an Array, you can use:
guard let myElement = myArray[safe: myIndex] else {
//error path where we leave scope
}
Great! We’ve just added an Array
extension that increases the amount of features, and safety, of our Arrays
.
However, there’s an even bigger advantage to extensions that we haven’t fully covered yet. Using these extensions, we can save ourselves a lot of repetition in the Business Logic spread throughout our apps.
For instance, if we intend to calculate the total ages of individuals across various households using the aforementioned reduce
function, we would typically find ourselves duplicating this code across our application:
let sumOfAges = arrayOfHouses.reduce(0) {
partialResult, next in
return partialResult + next.inhabitants.reduce(0, {
inhabitantsPartialResult, nextPerson in
return inhabitantsPartialResult + nextPerson.age
})
}
Instead of this laborious process, we can simply extend Array
, limiting the extension to the contained type:
extension Array where Element == House {
func summedAges() -> Int {
return arrayOfHouses.reduce(0) {
partialResult, next in
return partialResult + next.inhabitants.reduce(0, {
inhabitantsPartialResult, nextPerson in
return inhabitantsPartialResult + nextPerson.age
})
}
}
}
In the example the extension is applied to Swift Array with a constraint condition that the element type must be House. This means the summedAges
method will only be available to arrays whose elements are of type House.
Now, anywhere in our app that we need to know the sum of ages in any Array
of Houses
, we can simply write:
housesArray.summedAges()
We can do this with filters
, maps
, sort
, and any of the other operations that we’ve covered in this article.
Swift Arrays FAQ
Can you filter elements in a Swift array?
Swift Arrays can be filtered using the filter
method. For example, filtering people based on age or last name using conditions specified in a closure.
Can I check if a Swift array contains a specific element?
To check if a Swift array contains an element, you use the contains
method or use the filter
method we have seen before.
let vehicles = ["car", "motorcycle", "bicycle"]
let containsMotorcycle = vehicles.contains("motorcycle")
// This will print "true" because 'motorcycle' is in the array.
print(containsMotorcycle)
Can Swift arrays be nested?
Yes, you can have nested arrays in Swift, which means an array can contain other arrays as its elements creating a multidimensional array.
How does the Reduce
function work in Swift arrays?
The Reduce
function in Swift Arrays combines all elements into a single value. It’s typically used for calculations, like summing up numbers in an array.
Can Swift arrays be extended for additional functionalities?
Yes, Swift Arrays can be extended like any other class in Swift to include additional functionalities, such as safely accessing elements at specific index positions or do map and reduce calculations.
To Sum Up
Hopefully we’ve helped cast some light on the most advanced ways to deal with Swift Arrays. We have showed you how to sort them, filter them and transform them using all types of maps, how to Reduce them into one variable, and even how we can extend them to help us with our business logic.
You should also now have a clear understanding of which kind of map to use in various situations, and even how to extend the language itself to add support to any new functionality you may need to take your own logic further.
As a bonus, you will even have knowledge of some higher-order functions, which may provide the springboard to further exploration of functional programming.