In Swift, arrays enable us to sort, filter and transform our data efficiently. In fact, they’re among the most powerful and versatile data structures in our toolkit.
Swift arrays were introduced way back in 2014, but many developers find them confusing – particularly those who are used to reference types, or don’t fully grasp the possibilities that arrays provide. So in this guide, we’ll go beyond the basics and explore advanced Swift array operations like map, filter, reduce, and sorted, with practical examples and performance notes. By the end, you’ll know how to write cleaner, faster, and more expressive Swift code using arrays.
Table of Contents
- [What are Swift arrays and how they work](#what_are_swift_arrays_…
In Swift, arrays enable us to sort, filter and transform our data efficiently. In fact, they’re among the most powerful and versatile data structures in our toolkit.
Swift arrays were introduced way back in 2014, but many developers find them confusing – particularly those who are used to reference types, or don’t fully grasp the possibilities that arrays provide. So in this guide, we’ll go beyond the basics and explore advanced Swift array operations like map, filter, reduce, and sorted, with practical examples and performance notes. By the end, you’ll know how to write cleaner, faster, and more expressive Swift code using arrays.
Table of Contents
What are Swift arrays and how they work
In Swift, arrays are ordered collections that store multiple values of the same type. They’re value types, which means that when an array is assigned to another variable or passed to a function, Swift creates a copy instead of referencing the same instance.
You can think of this difference like sharing a document:
- Value types are like sending a copy by email — each person can edit their version without affecting the original.
- Reference types are like sharing a Cloud link — any change updates the original for everyone.
Reference types offer several advantages, to be sure. Multiple references can share the same underlying instance and value, while copies of reference types share the same underlying data, which reduces memory overhead.
However, because value types can be changed without affecting other instances, we can use them to modify data while ensuring predictable behavior and thread safety.
(A quick note before we go on: For basic array operations, you should visit our guide on Swift collections. This article is designed to explore advanced concepts and real-world uses).
Value-based nature of arrays (copy on write)
‘Copy on write’ is a technique used by Swift and a handful of other programming languages to avoid unnecessary data duplication. Rather than simply copying data when you assign it, Swift only makes a copy when a variable actively tries to change the data.
- Swift lets two (or more) variables share the same memory as long as nobody modifies it.
- When one of them tries to change the data, only then does a real copy occur — so each variable has its own unique version.
Here’s how this works for Swift 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
Swift arrays vs reference types: key differences
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 Swift 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.
Advanced Swift array operations (map, filter, reduce, sort)
Ok, we’ve looked at the theory behind Swift arrays, so let’s start looking at practical examples of what they can do.
We will explore 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’re looking at today will rely on a higher-order function. Let’s take a quick overview of the concept.
The term higher-order functions, or HoFs, comes from functional programming. It’s simply 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 returns a function, which itself calculates a factorial.
Got that? Great, now let’s move on to the specific functions.
Swift array sort: how to use sort() and sorted()
Sorting is a crucial part of array management. As we said at the top, the whole point of Swift arrays is to order our elements. We need a clear, consistent way of doing this.
Actually there are two principal methods we can use for sorting:
sort()sorts an array in place.sorted()gives us a sorted copy of the array, 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 whenever it fits your needs better.
There are lots of specific techniques we can use to sort arrays. For this demonstration, 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.
- Because Person conforms 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, sorting persons by age was simple, as age was stored as an Integer. Usually, we can achieve this through the person’s birth date. In this case, you might need to compare the two Swift dates using Swift date operations.
Swift array filter: practical filter() example
Another essential Swift array operation is filtering. This allows us to zoom into the elements that meet specific conditions, and extract only them.
We’re looking at one of Swift’s most useful higher-order functions, so let’s look at a few practical examples using 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)
Swift map, compactMap, and flatMap explained
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.
Map: transform elements
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 point, 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. However 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.
CompactMap: remove nils
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 two cars and no nil values. This is very helpful in cases where we do not want to have any nils after our transformation.
FlatMap: flatten nested arrays
The third and last kind of map is the flatMap. Like the others, it’s a 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 combines the inner array of the original map into one single array, removing any nested Collections or multidimensional arrays.
Swift reduce: combining array values efficiently
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 incorporate all elements in an array. Not into one single array like the flatMap, but into one single value.
This function is typically 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.
Swift array extensions: safe subscripting & utilities
In Swift, an extension lets you add new functionality to an existing type. When you write an array extension, you’re effectively customizing or enhancing the behavior of arrays throughout your codebase. This can bring some major time and efficiency savings.
Creating a Swift extension for arrays works just like extending any other class or structure in Swift. To demonstrate how it works, let’s look at a practical example.
We’re going to make an extension to fix an issue tha allt 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, whenever we need to know the sum of ages in any Array of Houses in our app, 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.
Key takeaways
- Swift
arraysare value-type data structures that copy on write, ensuring predictable behavior and thread safety. - We should use
sort()orsorted()to organize data, and choosefilter()to extract only the elements we need. map,compactMap, andflatMaptransform arrays efficiently for data conversion and cleanup.- We can combine or summarize values with
reduce()to produce a single result from multiple elements. - A Swift extension allows us to extend functionality safely and avoid repetitive code.
Hopefully, this guide helped you master the most advanced Swift array operations and understand when to use each technique effectively in real projects. But as always, if you have any further questions, we’re always happy to hear them.