Skip to content

Arrays in Swift

Array is an ordered, random-access collection type. Arrays are one of the most commonly used data types in an app. We use the Array type to hold elements of a single type, the array's Element type. An array can store any kind of elements—from integers to strings to classes.

In this guide, we will explore how arrays work under the hood, how to manipulate them, and how to apply functional programming concepts to transform data effectively.

Creation of Arrays

Before we can manipulate data, we must understand how to store it. Array is an ordered collection type in the Swift standard library. It provides O(1) random access and dynamic reallocation. Array is a generic type, so the type of values it contains are known at compile time.

As Array is a value type, its mutability is defined by whether it is annotated as a var (mutable) or let (immutable). The type [Int] (meaning: an array containing Ints) is syntactic sugar for Array<Element>.

Empty Arrays

There are several ways to declare an empty array. The following three declarations are equivalent:

// A mutable array of Strings, initially empty.
var arrayOfStrings: [String] = []      // type annotation + array literal
var arrayOfStrings = [String]()        // invoking the [String] initializer
var arrayOfStrings = Array<String>()   // without syntactic sugar

Array Literals

The most common way to create an array with initial data is by using an array literal, which is written with square brackets surrounding comma-separated elements:

let arrayOfInts = [2, 4, 7]

This creates an immutable array of type [Int] containing 2, 4, and 7. The compiler can usually infer the type of an array based on the elements in the literal, but explicit type annotations can override the default:

// type annotation on the variable
let arrayOfInt8s: [UInt8] = [2, 4, 7]

// type annotation on the initializer expression
let arrayOfInt8s = [2, 4, 7] as [UInt8]

// explicit for one element, inferred for the others
let arrayOfInt8s = [2 as UInt8, 4, 7]

Advanced Initialization

Sometimes you need to build arrays dynamically or pre-allocate them with specific values. For arrays with repeated values, you can use the repeating initializer:

let arrayOfStrings = Array(repeating: "Example", count: 3)
// → arrayOfStrings = ["Example", "Example", "Example"]

You can also create arrays from other sequences, such as the key-value pairs of a dictionary:

let dictionary = ["foo": 4, "bar": 6]
let arrayOfKeyValuePairs = Array(dictionary)
// → arrayOfKeyValuePairs = [("bar", 6), ("foo", 4)]

Multi-dimensional Arrays

When your data structure requires a grid or matrix, a multidimensional array is created by nesting arrays. For example, a 2-dimensional array of Int is [[Int]].

let array2x3 = [[1, 2, 3], [4, 5, 6]]
// → array2x3[0][1] = 2
// → array2x3[1][2] = 6

Nested Repeating Values

To create a multidimensional array of repeated values, use nested calls of the array initializer:

var array3x4x5 = Array(repeating: Array(repeating: Array(repeating: 0, count: 5), count: 4), count: 3)


Accessing Array Values

Once your array is populated, you need to retrieve its contents. The following examples will use this array to demonstrate accessing values:

                      0  1  2  3  4   index
var numbers: [Int] = [2, 3, 5, 7, 11]
         position    1  2  3  4  5

Subscripting and Safe Access

To access a value at a known index use the subscript syntax. Arrays use a zero based index which means the first element in the Array is at index 0.

let value0 = numbers[0] // → value0 = 2
let value2 = numbers[2] // → value2 = 5

Accessing an out-of-bounds index will crash your application. By adding the following extension to array, indices can be accessed without knowing if the index is inside bounds:

extension Array {
    subscript (safe index: Int) -> Element? {
        return indices.contains(index) ? self[index] : nil
    }
}

if let thirdValue = numbers[safe: 2] {
    print(thirdValue)
}

Finding Elements and Extremes

Instead of directly accessing an index, you often need to search for specific elements. It is possible to return the index of a given value, returning nil if the value wasn't found:

numbers.firstIndex(of: 7) // → Optional(3)

There are methods for the first, last, maximum or minimum value in an Array. These methods will returnnil` if the Array is empty.

numbers.first // Optional(2)
numbers.last  // Optional(11)
numbers.max() // Optional(11)
numbers.min() // Optional(2)

Minimum and Maximum Values with Custom Ordering

You may also use the min() and max() methods with a custom closure, defining whether one element should be ordered before another. This allows you to find the minimum or maximum element in an array where the elements aren't necessarily Comparable.

For example, with an array of vectors:

struct Vector2 {
    let dx: Double
    let dy: Double

    var magnitude: Double {
        return (dx * dx + dy * dy).squareRoot()
    }
}

let vectors = [
    Vector2(dx: 3, dy: 2),
    Vector2(dx: 1, dy: 1),
    Vector2(dx: 2, dy: 2),
]

let lowestMagnitude = vectors.min { $0.magnitude < $1.magnitude }
let highestMagnitude = vectors.max { $0.magnitude < $1.magnitude }

Modifying Values in an Array

As state changes in your application, your arrays need to adapt. There are multiple ways to append values onto an array:

var numbers = [1, 2, 3, 4, 5]
numbers.append(6) 
// → numbers = [1, 2, 3, 4, 5, 6]

var sixOnwards = [6, 7, 8, 9, 10]
numbers += sixOnwards 
// → numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

And similarly, you can remove values from an array using position-based methods:

numbers.remove(at: 3) // → numbers = [1, 2, 3, 5, 6, 7, 8, 9, 10]
numbers.removeLast()  // → numbers = [1, 2, 3, 5, 6, 7, 8, 9]
numbers.removeFirst() // → numbers = [2, 3, 5, 6, 7, 8, 9]
numbers.removeAll()   // → numbers = []

Removing Elements by Value

Generally, if we want to remove an element from an Array, we need to know its index to use the standard remove(at:) function. But what if you don't know the index and only have the value you want to delete?

Instead of manually searching for the index every time, Swift provides modern, idiomatic ways to handle this.

Removing All Occurrences

The most efficient way to remove elements that match a specific value or condition is using removeAll(where:). This method is safe, fast, and handles multiple occurrences in a single pass.

var tags = ["swift", "ios", "code", "ios", "programming"]
tags.removeAll { $0 == "ios" }
// → tags = ["swift", "code", "programming"]

Removing only the First Occurrence

There are cases where you might only want to remove the first instance of a specific value while leaving the rest of the array intact. For this, you can extend Array (specifically for Equatable elements) to make your code more expressive:

extension Array where Element: Equatable {
    /// Removes the first occurrence of the specified element.
    mutating func removeFirst(_ element: Element) {
        if let index = firstIndex(of: element) {
            remove(at: index)
        }
    }
}

var list = ["A", "B", "C", "B"]
list.removeFirst("B")

print(list) // Output: ["A", "C", "B"]


Sorting an Array

Sorting elements based on their values or custom properties is a fundamental operation. As Array conforms to Sequence, we can generate a new array of the sorted elements using a built-in sort method.

var array = [3, 2, 1]
let sorted = array.sorted() // → sorted = [1, 2, 3]

As Array conforms to MutableCollection, we can sort its elements in place.

array.sort() // → array = [1, 2, 3]

Note

In order to use the default sorted() or sort() methods without closures, the elements must conform to the Comparable protocol.

Sorting with a Custom Ordering

You may also sort an array using a closure to define whether one element should be ordered before another. This isn't restricted to arrays where the elements must be Comparable. For example, it doesn't make sense for a Landmark to be naturally Comparable, but you can still sort an array of landmarks by height or name.

struct Landmark {
    let name: String
    let metersTall: Int
}

var landmarks = [
    Landmark(name: "Empire State Building", metersTall: 443),
    Landmark(name: "Eiffel Tower", metersTall: 300),
    Landmark(name: "The Shard", metersTall: 310)
]

// sort landmarks by height (ascending)
landmarks.sort { $0.metersTall < $1.metersTall }

// create new array of landmarks sorted by name
let alphabeticalLandmarks = landmarks.sorted { $0.name < $1.name }

dump(landmarks)
print("--------------------")
dump(alphabeticalLandmarks)

Sorting an Array of Strings

Sorting strings properly can introduce edge cases. The most simple way is to use sorted():

let words = ["Hello", "Bonjour", "Salute", "Ahola"]
let sortedWords = words.sorted()
print(sortedWords) // Output: ["Ahola", "Bonjour", "Hello", "Salute"]

But there will be unexpected results if the elements in the array are not consistent with their casing:

let words = ["Hello", "bonjour", "Salute", "ahola"]
let unexpected = words.sorted()
print(unexpected) // Output: ["Hello", "Salute", "ahola", "bonjour"]

To address this issue, either sort on a lowercase version of the elements, or import Foundation and use NSString's comparison methods like caseInsensitiveCompare:

import Foundation

let sortedWords = words.sorted { $0.caseInsensitiveCompare($1) == .orderedAscending }
print(sortedWords) // Output: ["ahola", "bonjour", "Hello", "Salute"]

To properly sort Strings by the numeric value they contain, use compare with the .numeric option:

import Foundation

let files = ["File-42.txt", "File-01.txt", "File-5.txt", "File-007.txt", "File-10.txt"]
let sortedFiles = files.sorted { $0.compare($1, options: .numeric) == .orderedAscending }

print(sortedFiles)
// Output: ["File-01.txt", "File-5.txt", "File-007.txt", "File-10.txt", "File-42.txt"]


Grouping and Comparing Arrays

Often, you'll need to correlate your array data with other structures. Suppose we have a User struct and an array of users values. We can group the users by the city property to produce a dictionary:

struct User {
    let name: String
    let city: String
}

let users = [
    User(name: "Alice", city: "Barcelona"),
    User(name: "Bob", city: "Madrid"),
    User(name: "Charlie", city: "Barcelona")
]

let usersByCity = Dictionary(grouping: users, by: { $0.city })

dump(usersByCity)

Grouping using a keyPath

If the property is directly accessible, you can use a key path instead of a closure:

let usersByCity = Dictionary(grouping: users, by: \.city)

Comparing 2 Arrays with zip

The zip function accepts 2 sequences and returns a Zip2Sequence where each element contains a value from the first sequence and one from the second sequence. This is useful when you want to perform some kind of comparison between the n-th element of each Array.

let list0 = [0, 2, 4]
let list1 = [0, 4, 8]

// Check whether each value in list1
// is the double of the related value in list0.
let list1HasDoubleOfList0 = !zip(list0, list1)
    .filter { $0 != (2 * $1) }
    .isEmpty

print(list1HasDoubleOfList0) // Output: true


Transforming Arrays

Swift arrays excel in functional programming. We can use methods to transform arrays without writing cumbersome for-in loops.

Mapping Elements

As Array conforms to Sequence, we can use map(_:) to transform an array of A into an array of B using a closure. For example, we could use it to transform an array of Ints into an array of Strings like so:

let numbers = [1, 2, 3, 4, 5]
let words = numbers.map { String($0) }
print(words) // Output: ["1", "2", "3", "4", "5"]

Note

map(_:) will iterate through the array, applying the given closure to each element. The result of that closure will be used to populate a new array with the transformed elements.

Selecting Elements with filter

The filter(_:) method creates a new array containing only the elements that satisfy a given condition (predicate), which you provide as a closure.

You can filter complex types, such as a collection of Person objects, to find those who meet a specific criteria:

struct Person { 
  let name: String
  let age: Int 
}

let people = [
    Person(name: "Alice", age: 22),
    Person(name: "Bob", age: 41)
]

let youngPeople = people.filter { $0.age < 30 }
print(youngPeople.map {$0.name}) // Output: ["Alice"]

Lazy Filtering

If you are working with a very large collection and you don't need the resulting array immediately, consider using .lazy:

let largeRange = 1...1_000_000
let filtered = largeRange.lazy.filter { $0 % 2 == 0 }
print(filtered.count) // Output: 500000

Combining Elements with reduce

The reduce function is used to combine all elements of an array into a single value. It takes an initial value and a closure that describes how to combine each element with an "accumulator."

let numbers = [2, 5, 7, 8, 10, 4]

let sum = numbers.reduce(0) { accumulator, element in
    return accumulator + element
}

print(sum) // Output: 36

Tip

Since operators in Swift are also functions, you can make this even more concise. If you are simply adding numbers, you can pass the + operator directly:

let sum = numbers.reduce(0, +)

Better Performance

When combining elements into a new collection (like a Dictionary or another Array), use reduce(into:_:). This variant is more efficient because it modifies the accumulator in place instead of creating a new copy at every step.

let letters = ["a", "b", "c", "a", "b", "a"]
let counts = letters.reduce(into: [:]) { counts, letter in
    counts[letter, default: 0] += 1
}

print(counts) // Output: ["a": 3, "b": 2, "c": 1]

Handling Optionals and Nested Arrays

Sometimes transformations yield optional values or nested arrays. Swift provides specific tools to safely unpack these arrays.

Filtering out nil with compactMap

If you want to extract values of a given type and create a new array in a safe way, discarding all nil elements, use compactMap (formerly known as a variant of flatMap).

let strings = ["1", "foo", "3", "4", "bar", "6"]
let numbersThatCanBeConverted = strings.compactMap { Int($0) }
print(numbersThatCanBeConverted) // Output: [1, 3, 4, 6]

You can also use this ability to simply convert an array of optionals into an array of non-optionals:

let optionalNumbers: [Int?] = [nil, 1, nil, 2, nil, 3]
let numbers = optionalNumbers.compactMap { $0 }
print(numbers) // Output: [1, 2, 3]

Flattening Sequences with flatMap

There is also a version of flatMap(_:) that expects the transformation closure to return a sequence. Each sequence from the transformation will be concatenated, resulting in an array containing the combined elements.

For example, taking an array of prime strings and combining their characters into a single array:

let primes = ["2", "3", "5", "7", "11"]
let allCharacters = primes.flatMap { $0 }
print(allCharacters) // Output: ["2", "3", "5", "7", "1", "1"]

As flatMap(_:) will concatenate the sequences returned from the transformation closure calls, it can be used to flatten a multidimensional array, such as a 2D array into a 1D array. This can simply be done by returning the given element $0 (a nested array) in the closure:

let array2D = [[1, 3], [4], [6, 8, 10]]
let flattenedArray = array2D.flatMap { $0 }
print(flattenedArray) // Output: [1, 3, 4, 6, 8, 10]


Array Slices and Memory Management

One can extract a series of consecutive elements from an Array using a Range. Subscripting an array with a range returns an ArraySlice. This is a subsequence of the original array.

let words = ["Hey", "Hello", "Bonjour", "Welcome", "Hi", "Hola"]
let slice = words[2...4] 
// → slice = ["Bonjour", "Welcome", "Hi"]

While an ArraySlice conforms to the Collection protocol, it is meant for transient computations only.

Memory Leak Warning

An ArraySlice keeps a reference to the entire original array in memory. To avoid unnecessary memory usage, you should convert the slice back into an Array as soon as you finish working with it:

let selectedWords = Array(words[2...4])

Value Semantics

Finally, it is essential to understand how arrays behave in memory. Copying an array will copy all of the items inside the original array. Changing the new array will not change the original array.

var originalArray = ["Swift", "is", "great"]
var newArray = originalArray
newArray[2] = "awesome!"

// → originalArray = ["Swift", "is", "great"]
// → newArray = ["Swift", "is", "awesome!"]

Copied arrays will share the same space in memory as the original until they are changed. As a result of this there is a performance hit when the copied array is given its own space in memory as it is changed for the first time.

Comments