Skip to content
Swift Arrays: Map, Filter, Reduce & Sort Explained

19 Minutes

Swift Arrays: Map, Filter, Reduce & Sort Explained

Fix Bugs Faster! Log Collection Made Easy

Get started

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.

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.

  1. Swift lets two (or more) variables share the same memory as long as nobody modifies it.
  2. 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 arrays are value-type data structures that copy on write, ensuring predictable behavior and thread safety.
  • We should use sort() or sorted() to organize data, and choose filter() to extract only the elements we need.
  • map, compactMap, and flatMap transform 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.

Expect The Unexpected!

Debug Faster With Bugfender

Start for Free
blog author

Flávio Silvério

Interested in how things work since I was little, I love to try and figure out simple ways to solve complex issues. On my free time you'll be able to find me with the family strolling around a park, or trying to learn something new. You can contact him on Linkedin or GitHub

Join thousands of developers
and start fixing bugs faster than ever.