Advanced Swift Arrays: Explore Sort, Filter, Map and Reduce and more

Advanced Swift Arrays: Explore Sort, Filter, Map and Reduce and more

iOSSwift
Fix bugs faster! Log Collection Made Easy
START NOW!

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.

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 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 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 Classfrom 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.

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.

Expect the Unexpected! Debug Faster with Bugfender
START FOR FREE

Trusted By

/assets/images/svg/customers/cool/riachuelo.svg/assets/images/svg/customers/highprofile/intel.svg/assets/images/svg/customers/highprofile/macys.svg/assets/images/svg/customers/cool/napster.svg/assets/images/svg/customers/highprofile/oracle.svg/assets/images/svg/customers/highprofile/deloitte.svg/assets/images/svg/customers/projects/menshealth.svg/assets/images/svg/customers/projects/porsche.svg

Already Trusted by Thousands

Bugfender is the best remote logger for mobile and web apps.

Get Started for Free, No Credit Card Required