34 Minutes
Swift Arrays: Complete Guide to Sort, Filter, Map and Reduce
Fix Bugs Faster! Log Collection Made Easy
Swift arrays are one of the most commonly used collection types in iOS and macOS development. They provide ordered, type-safe storage for values and include powerful higher-order functions such as map, filter, reduce, and sort. With value semantics, copy-on-write optimisation, and a rich set of built-in operations, Swift arrays make it easier to write safe, efficient, and expressive code.
This guide will empower you to master the basics of Swift arrays and explore all the functionality they provide. You’ll learn how to:
- Write cleaner transformations using
map,filter,reduceandsorted. - Search and access elements safely without risking runtime crashes.
- Build reusable array extensions that eliminate repeated logic.
- Chain operations efficiently and know when lazy evaluation matters.
What is a Swift array?
A Swift array is an ordered, typed collection that stores multiple values of the same type in a single variable. Swift arrays are value types, not reference types: each variable behaves like an independent value, which makes programs easier to reason about and less prone to bugs.
Developers use Swift arrays in a variety of ways, including:
- Creating a simple, ordered list of similar values for a news app or social media feed.
- Collecting user inputs for shopping lists and to-do apps.
- Analyzing calculations and exporting data.
- Creating a subset for a given condition to support search, reporting and analytics features.
We’ll explore the use cases in more detail later in the article.
How to create a Swift array
The standard way to create an array is with square brackets. This is called an array literal and it’s what you’ll use in almost every real-world codebase.
Here’s an example:
let fruits = ["apple", "banana", "orange"] // array of strings
let scores = [95, 80, 72, 88] // array of integers
let empty: [String] = [] // empty array, type declared explicitly
When the array is empty, Swift needs you to declare the type explicitly. Otherwise it has no way of knowing what kind of values you intend to store.
You can also create a prefilled array using Array(repeating:count:), useful when you need a fixed-size structure with a default value.
let zeros = Array(repeating: 0, count: 5)
// [0, 0, 0, 0, 0]
Mutable vs immutable arrays
A mutable array can be modified after creation, giving your users the freedom to add or remove items. An immutable array stays fixed, important if you’re building a list of configuration options or supported languages. The let and var keywords allow you to determine which you want.
letmakes the array immutable.varmakes it mutable.
let fixed = ["a", "b", "c"]
// fixed.append("d") compile error
var dynamic = ["a", "b", "c"]
dynamic.append("d")
// ["a", "b", "c", "d"]
Important to note: You can start with let and switch to var when you actually need to modify the array. This forces intentional mutation and prevents a whole class of bugs where data changes unexpectedly.
Multidimensional arrays and arrays of tuples
A multidimensional array is an array where each element is itself an array. This is useful for grid-like structures such as a game board, a data table, or a matrix.
let grid: [[Int]] = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
print(grid[1][2]) // 6, row 1 column 2
You access elements by chaining two indexes. The first picks the row, the second picks the column.
For simple pairs, you may feel that defining a full struct is too verbose or time-consuming. In these cases, you can use tuples (small groups of values stored together as a single value) to speed things up.
However if your data has more than two or three fields, or needs to be passed between functions, a named struct is the better choice.
let coordinates: [(x: Int, y: Int)] = [(1, 2), (3, 4), (5, 6)]
print(coordinates[0].x) // 1
print(coordinates[0].y) // 2
How to manipulate Swift arrays
Swift arrays are designed to be flexible, and you may want to manipulate them to simplify your code, improve code readability, and make your data-handling logic more efficient as your app requirements change.
You have seven principal options to manipulate your arrays.
| Method | What it does |
|---|---|
sort() | Sorts the array in place, mutates the original |
sorted() | Returns a new sorted array, original unchanged |
filter() | Returns a new array with only matching elements |
map() | Transforms each element, returns same-length array |
compactMap() | Transforms each element, removes nil results |
flatMap() | Transforms each element into a sequence, flattens into one array |
reduce() | Collapses all elements into a single value |
sort() and sorted()
Swift gives us two [potential sorting methods, with an important distinction.
sort()sorts the array in place and mutates the original.sorted()returns a new sorted array and leaves the original unchanged.
var numbers = [5, 2, 8, 1, 9]
numbers.sort()
// numbers is now [1, 2, 5, 8, 9]
let numbers2 = [5, 2, 8, 1, 9]
let sorted = numbers2.sorted()
// sorted = [1, 2, 5, 8, 9], numbers2 unchanged
For descending order, pass a comparator closure.
let descending = numbers2.sorted(by: >)
// [9, 8, 5, 2, 1]
For custom objects, you have two options: make the type conform to Comparable, or pass a closure directly to sorted(by:).
struct Player {
let name: String
let score: Int
}
let players = [
Player(name: "Alice", score: 80),
Player(name: "Bob", score: 95),
Player(name: "Carol", score: 72)
]
// Option 1: closure (one-off sort)
let ranked = players.sorted { $0.score > $1.score }
// Option 2: Comparable conformance
extension Player: Comparable {
static func < (lhs: Player, rhs: Player) -> Bool {
lhs.score < rhs.score
}
}
let ranked2 = players.sorted()
filter()
filter() returns a new array containing only the elements that satisfy a condition. Note that it never mutates the original.
let numbers = [1, 2, 3, 4, 5, 6]
let even = numbers.filter { $0 % 2 == 0 }
// [2, 4, 6]
For object arrays, the closure receives the full element.
let activeUsers = users.filter { $0.isActive }
let highScorers = players.filter { $0.score >= 80 }
You can store the predicate as a variable and reuse it across multiple filter calls.
let isHighScorer: (Player) -> Bool = { $0.score >= 80 }
let topPlayers = players.filter(isHighScorer)
map(), compactMap() and flatMap()
map(), compactMap(), and flatMap() all help transform Swift arrays, making your code more concise and readable. The difference comes down to the shape of the resulting array and how you want to handle missing values or nested collections.
map() returns a new array of the same length.
let names = ["alice", "bob", "carol"]
let capitalized = names.map { $0.capitalized }
// ["Alice", "Bob", "Carol"]
compactMap() automatically discards any nil results. Use it when some elements may not produce a valid transformed value.
let strings = ["1", "two", "3", "four"]
let integers = strings.compactMap { Int($0) }
// [1, 3]
flatMap() combines the resulting collections into a single flattened array. Use it when your transformation creates nested arrays that you want to merge into a single array.
let sentences = ["Hello world", "Swift is great"]
let words = sentences.flatMap { $0.split(separator: " ") }
// ["Hello", "world", "Swift", "is", "great"]
reduce()
reduce() collapses an entire array into a single value by applying an accumulator function across each element. It takes two arguments: the initial value, and a closure that receives the running result and the next element.
let numbers = [1, 2, 3, 4, 5]
let sum = numbers.reduce(0, +)
// 15
For object arrays, reduce() is particularly useful for aggregating computed properties.
let totalScore = players.reduce(0) { $0 + $1.score }
How to work with Swift array elements
Swift arrays are designed to group elements together and make code more manageable. However we may occasionally need to access individual pieces of data, so we can inspect and update them.
Swift gives us several different ways to search, combine, and manipulate specific elements in our arrays.
| Method | What it does |
|---|---|
contains() | Checks if a value exists, returns a boolean |
firstIndex(where:) | Returns the index of the first matching element |
+ / append(contentsOf:) | Merges two arrays, mutating or non-mutating |
joined(separator:) | Converts an array to a string |
[safe: index] | Returns nil instead of crashing on out-of-bounds access |
contains() and firstIndex()
For simple Equatable types, contains() takes a value directly.
let fruits = ["apple", "banana", "orange"]
fruits.contains("banana") // true
fruits.contains("mango") // false
For objects or complex conditions, use the closure version.
let hasHighScorer = players.contains { $0.score >= 90 }
If you need the index rather than a boolean, use firstIndex(where:).
let index = players.firstIndex { $0.name == "Bob" }
Joining and converting arrays
The + operator and append(contentsOf:) both merge arrays but behave differently.
+returns a new array.append(contentsOf:)mutates the original.
let a = [1, 2, 3]
let b = [4, 5, 6]
let merged = a + b
// [1, 2, 3, 4, 5, 6]
var mutable = [1, 2, 3]
mutable.append(contentsOf: b)
// mutable = [1, 2, 3, 4, 5, 6]
To convert an array to a string, use joined(separator:).
let words = ["Swift", "is", "expressive"]
let sentence = words.joined(separator: " ")
// "Swift is expressive"
Safe element access
If you try to access an out-of-bounds index, you’ll find that this causes a runtime trap (Index out of range). The standard library provides no safe subscript by default, but you can add one with an extension.
extension Array {
subscript(safe index: Index) -> Element? {
indices.contains(index) ? self[index] : nil
}
}
let items = ["a", "b", "c"]
print(items[safe: 5]) // nil, no crash
This pattern is especially useful when working with dynamic data where the index cannot be guaranteed at compile time.
Advanced Swift array techniques
As your app scales, your arrays can become complex and you may need to find efficiencies in your coding playbook. In these situations you may wish to use more powerful array-handling techniques, which can help you cut down on unnecessary code and reuse common behaviors.
The following techniques are ideal for experienced Swift developers who wish to restructure array-heavy code for clarity, performance, and reusability.
| Technique | What it does |
|---|---|
| Custom extensions | Encapsulate repeated array logic into reusable, type-safe methods |
| Chaining | Compose multiple operations in a single expression without intermediate variables |
lazy evaluation | Processes each element once through the full chain, avoids intermediate arrays |
Custom array extensions
Extensions let you encapsulate repeated array logic once and reuse it everywhere. This eliminates duplication when the same filter + map chain appears across multiple files.
extension Array where Element == Player {
func topScorers(above threshold: Int) -> [Player] {
filter { $0.score >= threshold }
.sorted { $0.score > $1.score }
}
}
let leaders = players.topScorers(above: 80)
The where Element == constraint limits the extension to arrays of a specific type, so it only appears where relevant. This keeps your autocompletion clean and prevents misuse.
You can also write generic extensions using protocol constraints.
extension Array where Element: Numeric {
var sum: Element {
reduce(0, +)
}
}
[1, 2, 3, 4].sum // 10
[1.5, 2.5, 3.0].sum // 7.0
Chaining and lazy evaluation
Swift array methods are designed to chain together. Each method returns a new array, so you can compose complex transformations in a single expression without intermediate variables.
let result = players
.filter { $0.score >= 70 }
.sorted { $0.score > $1.score }
.map { "\($0.name): \($0.score)" }
// ["Bob: 95", "Alice: 80", "Carol: 72"]
Each method in the chain iterates the full array, creating an intermediate array at each step. For large datasets, use lazy to defer evaluation and process each element only once through the full chain.
let result = players.lazy
.filter { $0.score >= 70 }
.map { $0.name }
Now that we’ve explored the core tools and techniques, let’s talk about how to use them in the real world.
Swift arrays in practice
Swift arrays can be deployed in practically any type of data-heavy application, from social media apps to
Building a leaderboard from raw data
A game app receives a raw list of players from an API. Some entries have invalid scores, some players are inactive, and the display requires a formatted string for each player. Here is how to handle the full pipeline in one chain.
struct Player {
let name: String
let score: Int?
let isActive: Bool
}
let raw: [Player] = [
Player(name: "Alice", score: 80, isActive: true),
Player(name: "Bob", score: nil, isActive: true),
Player(name: "Carol", score: 95, isActive: true),
Player(name: "Dave", score: 72, isActive: false),
Player(name: "Eve", score: 88, isActive: true)
]
// Step 1: keep only active players with a valid score
let eligible = raw.filter { $0.isActive && $0.score != nil }
// Step 2: sort by score descending
let ranked = eligible.sorted { $0.score! > $1.score! }
// Step 3: format for display
let leaderboard = ranked.map { "\($0.name): \($0.score!)pts" }
// Step 4: calculate total score across eligible players
let totalPoints = eligible.reduce(0) { $0 + ($1.score ?? 0) }
print(leaderboard)
// ["Carol: 95pts", "Eve: 88pts", "Alice: 80pts"]
print(totalPoints)
// 263
Each step uses a single method with a clear responsibility. No intermediate mutation, no temporary variables.
Processing an API response
A social app receives a raw JSON response with user tag arrays for each post. Some tag fields are missing or malformed. The goal is a deduplicated, sorted list of all unique tags across all posts for a filter UI.
struct Post {
let id: Int
let rawTags: String? // comma-separated, may be nil or malformed
}
let posts: [Post] = [
Post(id: 1, rawTags: "swift, ios, mobile"),
Post(id: 2, rawTags: nil),
Post(id: 3, rawTags: "swift, xcode"),
Post(id: 4, rawTags: "ios, debugging, mobile")
]
// Step 1: extract tag strings, skip nil entries
let tagStrings = posts.compactMap { $0.rawTags }
// Step 2: split each string into individual tags and flatten
let allTags = tagStrings.flatMap {
$0.split(separator: ",").map { $0.trimmingCharacters(in: .whitespaces) }
}
// Step 3: deduplicate using Set, convert back to array
let uniqueTags = Array(Set(allTags))
// Step 4: sort alphabetically for the UI
let sortedTags = uniqueTags.sorted()
print(sortedTags)
// ["debugging", "ios", "mobile", "swift", "xcode"]
// Step 5: check if a specific tag exists before rendering a filter button
let showSwiftFilter = sortedTags.contains("swift")
// true
Each method handles one concern cleanly: no guard statements, no manual loops, no temporary variables.
Common Swift array problems
If an array method is not behaving as expected, the cause is usually one of these.
| Symptom | Cause and fix |
|---|---|
Numbers sort in the wrong order after sort() | Default sort is lexicographic, not numeric. Use sort { $0 < $1 } or sorted(by: <) instead |
map() returns [Optional<T>] instead of [T] | Your transform returns nil for certain elements. Switch to compactMap() to strip nils automatically |
contains() returns false on object arrays | Objects must conform to Equatable or you should use contains(where:) to compare properties. |
| Original array changed unexpectedly after sorting | sort() mutates in place. Use sorted() to return a new array without touching the original |
map() or forEach() silently skips elements | Check that the source array actually contains the expected values. Swift arrays never contain empty slots; Array(repeating:count:) creates real elements. |
| Method chain is slow or causes memory warnings | Each chained method creates a full intermediate array. Prefix the chain with .lazy to process elements one at a time |
flatMap() returns optionals instead of flattening | You need nil removal, not flattening. Use compactMap() for optionals and flatMap() only when your transform returns a sequence |
| UI not updating after modifying a SwiftUI array | The array was mutated directly instead of reassigned. SwiftUI only detects changes on reassignment, so return a new array using filter(), map(), or spread syntax |
Best practices for Swift arrays
- Default to
let: Declare arrays as constants unless mutation is explicitly required. This prevents accidental state changes. - Prefer
sorted()oversort(): The non-mutating version is safer in most contexts. - Replace
map()+ filter withcompactMap(): It expresses the same intent in one step and removes the intermediate optional array. - Avoid
[Any]arrays: Mixing types loses Swift’s type safety. Use enums with associated values or generics instead. - Wrap repeated logic in extensions: If the same filter-sort-map chain appears more than twice, it belongs in an extension.
- Apply
lazyfor large datasets: When chaining multiple operations on arrays with thousands of elements,lazyavoids intermediate arrays and reduces memory pressure. - Reach for
firstIndex(where:)over manual loops: When you need a position in the array, the built-in method is cleaner and less error-prone.
Key takeaways
Swift arrays are one of the most frequently used data structures in iOS and macOS development. Getting comfortable with the full range of methods, from filter() and map() to reduce() and custom extensions, is what makes the difference between code that works and code that is easy to read, test, and maintain.
The patterns covered in this tutorial map directly to real production scenarios: transforming API responses, filtering lists, aggregating data, and building reusable utilities. Start with the fundamentals, layer in the advanced techniques as your codebase grows, and reach for extensions whenever you notice the same logic appearing in more than one place.
Frequently asked questions
When should I use compactMap instead of map in Swift?
Use compactMap() when your transformation can return nil. It removes nil results automatically, giving you a clean [T] instead of [T?]. A common case is converting [String] to [Int] where some strings are not valid numbers. Use map() when every input produces an output and you want to preserve the original number of elements.
What is the difference between flatMap and compactMap in Swift?
compactMap() transforms values and removes any nil results from optional transformations. flatMap() transforms values and flattens nested collections into a single collection. In short, compactMap() cleans your data by removing missing values, while flatMap() simplifies your structure by removing unnecessary nesting. They solve different problems and are not interchangeable.
What is the difference between sort() and sorted() in Swift?
sort() mutates the original array, while sorted() returns a new sorted array without changing the original. Use sort() when mutation is acceptable and sorted() when you want to preserve the original data.
How do I safely access an element at an index in Swift?
Swift does not provide a safe array subscript by default. A common approach is to add an array extension that allows you to safely access an element without crashing. If the index is outside the array bounds, the extension returns nil instead of causing an Index out of range error.
extension Array {
subscript(safe index: Index) -> Element? {
indices.contains(index) ? self[index] : nil
}
}
Particularly valuable in table views, paginated lists, or any context where array length changes at runtime.
How do I remove duplicates from a Swift array?
Use a Set to remove duplicates from a Swift array when the order of elements does not matter. A Set stores only unique values, so converting an array to a set automatically removes duplicate elements.
let values = [1, 2, 2, 3, 3, 3]
let unique = Array(Set(values))
Note that Set does not preserve order. If order matters, use a deduplication extension with an index check instead.
How does lazy evaluation improve Swift array performance?
Lazy evaluation delays computation until its result is actually needed. Without lazy, each chained method creates a full intermediate array. With lazy, elements flow through the entire chain one at a time, so no intermediate arrays are allocated. This reduces both memory usage and iteration count on large datasets.
Does sorted() work on struct arrays without Comparable?
Yes. Pass a closure to sorted(by:) and Swift does not require Comparable conformance. Conformance is only needed when calling sorted() with no arguments, which requires the type to define its natural ordering via the < operator.
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.
- 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.
Expect The Unexpected!
Debug Faster With Bugfender