Photo by Walkator on Unsplash

Collections in Swift

Maria Eduarda Casanova
Poatek
Published in
9 min readSep 13, 2023

--

In Swift, a collection is a data structure that can hold multiple values of the same type. It allows you to group related data together and perform operations on them efficiently. Swift provides several collection types, each with its own characteristics and purposes.

Arrays

One of the commonly used collection types in Swift is an array. It is an ordered collection that stores multiple values of the same type. You can access elements in an array by their index, and you can add, modify, or remove elements in the array.

In Swift, arrays, along with all other collection types in the standard library, exhibit value semantics, which relates to how values are copied and shared in memory. When you assign an existing array to a different variable, a copy of the array’s contents is made. This ensures that any modifications made to the new variable do not affect the original array.

There are many ways to iterate over an array, and most of them follow functional programming patterns. It’s hard to introduce the idea of how we can use arrays in Swift without talking about higher-order functions. Briefly, higher-order functions are functions that receive other functions as arguments.

The reasons for iterating over an array are infinite, the array collection offers a lot more than the good and old for loop. When the right tools are used, it can be a lot easier to understand what the code is doing in the meantime it becomes more effective.

Is possible to iterate over an array and apply more constraints to it at the same time. For example, given an array of random numbers:

let array = [ 2, 1, 3, -7, 3, 9, -2]

It’s possible to iterate over all but the first element with; • for x in array.dropFirst()
It’s possible to iterate over all but the last 3 elements;
• for x in array.dropLast(3)

It’s possible to number each element of the array;
• for (num, elem) in array.enumerated()
It’s possible to iterate over all but the first element with;
• for x in array.dropFirst()
It’s possible to iterate over only the elements that match specific criteria; • for x in array.filter { someCriteria($0) }

Different from iterating over an index, when the developer should be careful about the bounds of it, some operations that will be listed below have their return as an optional value if the array is empty. Providing more safety for the code.

Following the idea of using functional programming to improve the code, the standard library has many functions that have a parameterization behavior, i.e. they can separate the logic of how to transform each element from the code that doesn’t change from call to call.

Some of these functions are described below:

map and flatMap — transform the elements

filter — include only certain elements
reduce — fold the elements into an aggregate value

allSatisfy — test all elements for a condition

forEach — visit each element
sort(by:), sorted(by:) and partition(by:) — reorder the elements

firstIndex(where:), lastIndex(where:), first(where:), last(where:), and contains(where:) -does an element exist?
min(by:) and max(by:) — find the minimum or maximum of all elements

elementsEqual(_:by:) and starts(with:by:) — compare the elements to another array

split(whereSeparator:) — break up the elements into multiple arrays
prefix(while:) — take elements from the start as long as the condition holds true

drop(while:) — drop elements until the condition ceases to be true, and then return the rest

removeAll(where:) — remove the elements matching the condition

Each of those functions is useful for some reason, and they have different returns between them. Some return a new array with the result, and others return the value of the result itself. Valid information about them is that some of them use generics in their signature. This means that you can use different types for the same operation, which makes them more reusable, type safety and flexible.

Let’s dive into all those functions starting from the map. Transforming all elements from an array is a very common task. Using a map to perform it is a shorter manner, more precise and there’s less space for mistakes. It returns an array containing the results of mapping the given closure over the sequence’s elements. An important thing to keep in mind is that map is not made to perform mutations on other variables, it will hide side effects. The complexity of this is O(n), where n is the length of this sequence.

var numbers = [1, 2, 3, 4]

let mapped = numbers.map { Array(repeating: $0, count: $0) }
mapped // [ [1], [2, 2], [3, 3, 3], [4, 4, 4, 4] ]

The flatMap has all the benefits of the map, but its purpose is a little different. It will always return an array containing the concatenated results of calling the given transformation with each element of the sequence. The complexity of this is O(m + n), where n is the length of this sequence and m is the length of the result.

let flatMapped = numbers.flatMap { Array(repeating: $0, count: $0) } 
flatMapped // [1, 2, 2, 3, 3, 3, 4, 4, 4, 4]

The filter returns an array with all elements that satisfy some condition and the complexity is linear. The advantage of the power of combining map and filter is that is possible to significantly shorten and enhance the readability of the code by eliminating the need for intermediate variables in array operations. Its complexity is O(n), where n is the length of the sequence.

let squaredEvens = numbers.map { $0 ** 2 }.filter { $0 % 2 == 0 } 
squaredEvens // [ 4, 16]

The map and filter return a new array with the results, in some cases, the necessity is to combine all values into one. For those cases the method reduce is the answer. It collects all values in the array and applies some logic for all of them, returning the final accumulated value. It takes two arguments: the initial value of the accumulator, and a closure that defines how the next value should be combined with the current accumulator value. The output type of reduce doesn’t have to be the same as the element type, it’s possible to transform an array of integers in a string, for example. Its complexity is O(n), where n is the length of the sequence. It can be written in two ways, the initial value is set to 0 using the reduce method’s first parameter in both. The logic that will be applied can be passed an explicit closure or an operator.

let sum = numbers.reduce(0) { total, num in total + num } sum // 10
let sum = numbers.reduce(0, + ) sum // 10

Inside the closure, the total represents the accumulated value so far, and num represents each element of the array being iterated over. The closure adds each num to the total and returns the updated total for the next iteration. This process continues until all the elements in the array have been processed.

The allSatisfy does exactly what the name says, it take an array and verifies if all elements satisfy some condition. It returns a boolean type and the complexity is linear.

let result = numbers.allSatisfy { isEven($0) } 
result // false

The forEach is similar to the conventional for loop, but it will always pass for all elements, because of that its complexity is O(n), where n is the length of the sequence. This one doesn’t return anything unlike the others, it is specifically meant for performing side effects.

numbers.forEach { print($0) } // [1, 2, 3, 4]

The sort(by:) takes an argument a closure that defines the order that has the logic to compare the elements inside the array, those don’t need to conform to the comparable protocol. It mutates the original array directly without creating a new sorted array. The elements are re-arranged in ascending order based on the provided closure.

numbers.sort(by: > ) 
numbers // [4, 3, 2, 1]

The sorted(by:) does the exact same thing as the sort(by:), although it creates a new array instead of mutating the original. Both their complexities are O(n log n), where n is the length of the sequence.

let result = numbers.sorted(by: >) 
result // [1, 2, 3, 4]
numbers // [4, 3, 2, 1]

The partition(by:) reorders the elements of the collection such that all the elements that match the given logic are after all the elements that don’t match. The result is a pivot index p where no element before p satisfies the given logic and every element at or after p satisfies it. This is a mutating function, besides the index, it also changes the order of the array. This operation isn’t guaranteed to be stable, so the relative ordering of elements within the partitions might change. Its complexity is O(n), where n is the length of the collection.

let p = numbers.partition(by:{ $0 < 2 })
p // 3
numbers // [4, 2, 3, 1]

let first = numbers[..<p]
first // [4, 2, 3]

let second = numbers[p…]
second // [1]

The firstIndex(where:) and lastIndex(where:) have both the same logic, the difference is that one returns the first index that matches the logic and the other the last one. The first(where:) and last(where:) are very similar to the others but they don’t return the index, but the element that satisfies the logic. If there’s not a match they return nil. The contains(where:) says if an element

exists or not in the array, it returns true or false. Their complexities are O(n), where n is the length of the collection.

let haveOne = numbers.contains(1)
haveOne // true

The min(by:) and max(by:) find the minimum or maximum value of all elements using some logic to compare them. If there’s not a match they return nil. Their complexities are O(n), where n is the length of the collection.

let numbers = [-5, 2, -8, -1, 10, -20]
let minAbsValue = numbers.min(by: { abs($0) < abs($1) })
minAbsValue // -1

let maxAbsValue = numbers.max(by: { abs($0) < abs($1) })
maxAbsValue // -20

The elementsEqual(_:by:) and starts(with:by:) determine whether one array and another are the same or start with the same elements of the other by using a closure for comparison.

let array = [(1, "One"), (2, "Two"), (3, "Three"), (4, "Four")]
let otherArray = [(1, "One"), (2, "Two")]
let startsWith = array.starts(with: otherArray, by: { $0.0 == $1.0 && $0.1 == $1.1 })
startsWith // true

The split(whereSeparator:) breaks up the array into multiple elements using a closure indicating whether the collection should be split at that element. The complexity is O(n), where n is the length of the collection.

let sentence = "Hello, world! How are you today?" 
let words = sentence.split { character in
return character.isWhitespace || character.isPunctuation }
words // ["Hello", "world", "How", "are", "you", "today"]

The prefix(while:) and drop(while:) methods are used to extract elements from a sequence based on a condition. The main difference between them is what they return. The prefix(while:) returns elements from the start as long as the condition holds true and the drop(while:) drops elements until the condition ceases to be true, and then returns the rest. Their complexities are O(n), where n is the length of the collection.

let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9] 
let prefix = numbers.prefix { $0 < 5 }
prefix // [1, 2, 3, 4]

let dropElements = numbers.drop { $0 < 5 }
dropElements // [5, 6, 7, 8, 9]

The removeAll(where:) method removes every element in an array that meets particular criteria. The order of the remaining elements is preserved. It’s a mutating method, it doesn’t create a new variable with the result. The complexity is O(n), where n is the length of the collection.

var numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9] 
numbers.removeAll { $0 % 2 == 0 }
numbers // [1, 3, 5, 7, 9]

Those were some of the most interesting methods that the standard library of the array collection provides. It was a simple explanation and an example of what it’s possible to do each of them. There are many more uses for those useful tools.

Dictionaries

Another collection type in Swift is the dictionary. When you require a collection that associates values with unique keys, you can use it. A dictionary is an unordered collection that stores key- value pairs. The keys within a dictionary must be unique and are used to retrieve the associated values efficiently.

The benefits of using dictionaries are the time to retrieve values on it, and the average time for consuming it is constant. The return is always an optional value, and if doesn’t exist it returns nil. Dictionaries are hash tables, which means that the key is stored based on its hashValue. It requires that the type of the key to conform to the Hashable protocol. All the basic types in Swift already do that, but adding a manual implementation to conform to it is possible.

Sets

Another collection type in Swift is a set. A set is an unordered collection of unique values of the same type. It is useful when you want to check for membership or eliminate duplicates from a collection. Like a dictionary, Set is implemented using a hash table and has similar performance and requirements. Sets in Swift are very related to the mathematical concept of a set. And it provides fast operations like union, intersection, and difference. Almost all set operations have both non-mutating and mutating forms, and the latter has a form prefix.

--

--

Maria Eduarda Casanova
Poatek
Editor for

Computer Science Graduate | iOS Senior Engineer at Poatek