What collection types are available in Swift, and when would you use each one?

Explain Swift's protocol-driven collection model, then compare Array, Set, Dictionary, and Range by behavior, complexity, hashing, ordering, copy-on-write, and thread-safety tradeoffs

Answer

The core idea

Swift collections are built around a protocol hierarchy, not just a fixed list of concrete types. That is an example of protocol-driven development: algorithms can be written once against Sequence, Collection, BidirectionalCollection, or RandomAccessCollection, then reused by Array, Set, Dictionary, ranges, strings, and custom collection types.

The short interview answer is: use Array when order and index-based access matter, Set when uniqueness and fast membership matter, Dictionary when values are looked up by key, and Range when you need to represent or iterate over a continuous interval.

1. Swift collections are protocol-driven

Swift's standard library separates what a type can do from how it stores data:

ProtocolWhat it promises
IteratorProtocolProduces one element at a time with next()
SequenceCan be iterated with for-in; may be single-pass
CollectionMulti-pass sequence with stable indices and startIndex / endIndex
BidirectionalCollectionCan move backward with index(before:)
RandomAccessCollectionCan jump by offsets efficiently

This matters because APIs can ask for the weakest useful capability. For example, map only needs a Sequence, but binary search needs at least a sorted random-access collection to be efficient.

swift
func printAll<S: Sequence>(_ values: S) where S.Element == String {
    for value in values {
        print(value)
    }
}

printAll(["Array", "Set", "Dictionary"])

Run compiles and executes on the server; output shows below.

2. Array: ordered random-access storage

An Array<Element> stores values in order and supports integer-indexed access. It is the default choice for lists, UI models, ordered results, and anything where position matters.

OperationAverage complexityNotes
Read by indexO(1)Random access by integer index
Append at endAmortized O(1)Occasional resize copies storage
Insert/remove at endO(1)Usually cheap at the tail
Insert/remove at front or middleO(n)Elements after the position shift
Search with contains / firstIndexO(n)Linear scan unless separately indexed
SortO(n log n)Comparison-based sorting

Arrays use dynamic contiguous storage. The buffer grows as needed, usually by reserving more capacity than the current count so repeated appends stay amortized O(1). If the final size is known, reserveCapacity(_:) can reduce reallocations.

swift
var names = ["Ana", "Ben"]
names.append("Chloe")

print(names[0])
print(names)

Run compiles and executes on the server; output shows below.

Swift arrays are value types with copy-on-write storage. Assigning an array usually shares the same element buffer at first; Swift copies the buffer only when one of the shared values is mutated.

Not runnable here as a fixed-output example: the exact pointer values change from run to run. The important part is that the first two addresses match before mutation, then diverge after mutation.

swift
import Foundation

func print<T>(address array: [T]) {
    array.withUnsafeBufferPointer {
        print($0.baseAddress!)
    }
}

var array1: [Int] = [0, 1, 2, 3]
var array2 = array1

print(address: array1)
print(address: array2) // Same buffer

array2[0] = 99 // Copy-on-write happens here

print(address: array1)
print(address: array2) // Different buffer

Run compiles and executes on the server; output shows below.

That value-semantic API does not make arrays automatically thread-safe. If two threads mutate the same array storage, the program has a data race. Even changing different elements by index is not a safe exception, because mutation can still interact with array storage, uniqueness checks, reference counts, and collection metadata.

3. Set: unique values with fast membership

A Set<Element> stores unique values where Element conforms to Hashable. Use it for membership checks, deduplication, selected IDs, visited nodes, feature flags, and operations such as union and intersection.

OperationAverage complexityNotes
InsertO(1)Requires hashing and equality
RemoveO(1)Finds the bucket, then removes
ContainsO(1)Main reason to choose a set
Iterate all valuesO(n)Order is not part of the contract
Union/intersectionUsually O(n + m)Based on the input sizes

Set is backed by a hash-table-style data structure. A hash table stores elements in an array-like storage area and uses a hash value to choose where an element should live. That is why membership can usually be checked without scanning every element.

Hashable has two important rules: equal values must produce the same hash, and equality is still checked after hashing. Different values can have the same hash; that is a collision. Collisions are allowed and handled internally, but too many collisions can degrade performance toward linear behavior.

Conceptually, a hash table converts a hash value into a storage bucket. Sometimes different values map to the same bucket; older explanations often describe that bucket as a linked list of entries. Each entry still stores the original value, so lookup is not based on the hash alone. The set compares candidates with == to decide whether the value is already present.

swift
let selectedIDs: Set<Int> = [10, 20, 30]

print(selectedIDs.contains(20))
print(selectedIDs.contains(99))

Run compiles and executes on the server; output shows below.

Set order is unspecified. It can appear stable in a small test, but code should not depend on it. Iteration order may change after inserts or removals, after the set resizes, across launches, and across Swift versions. If display order matters, convert to an array and sort with an explicit rule.

4. Dictionary: key-based lookup

A Dictionary<Key, Value> stores values by unique keys, where Key conforms to Hashable. Use it for caches, lookup tables, grouping by ID, metadata by identifier, and any state where a key is the natural way to find the value.

OperationAverage complexityNotes
Lookup by keyO(1)Hash key, then check equality
Insert/update by keyO(1)May resize storage
Remove by keyO(1)Removes the matching key-value pair
Iterate all pairsO(n)Order is not guaranteed
Search by valueO(n)Values are not indexed

Dictionary keys follow the same hashing rules as sets. A collision does not mean the dictionary loses data; it means multiple keys landed in the same hash area and Swift must use equality to find the exact key. Bad or unstable hashing hurts performance and correctness.

swift
var scoresByUserID: [Int: Int] = [
    42: 100,
    73: 95
]

scoresByUserID[42] = 101

print(scoresByUserID[42] ?? 0)

Run compiles and executes on the server; output shows below.

Dictionary order is also unspecified. Mutation, resizing, process launch, and implementation changes can all affect iteration order. If the UI needs sorted keys or stable sections, derive an ordered array explicitly:

swift
let sortedIDs = scoresByUserID.keys.sorted()

Like arrays and sets, dictionaries are value types with copy-on-write storage, but shared mutable access is not thread-safe. Concurrent reads can be fine only when no thread is mutating the dictionary. Once mutation is possible, use synchronization.

5. Range: intervals, loops, and slices

A range represents a continuous interval between bounds. Common forms include half-open ranges like 0..<10, closed ranges like 1...5, and one-sided ranges like ..<endIndex.

Ranges are most often used for loops, slicing, pagination, validation, and expressing index intervals.

swift
for page in 1...3 {
    print("Load page \(page)")
}

Run compiles and executes on the server; output shows below.

OperationTypical complexityNotes
Create a rangeO(1)Stores bounds, not every value
Check containsO(1)Compares against lower and upper bounds
Iterate an integer rangeO(n)Produces each value in the interval
Slice with a valid rangeUsually O(1) view creationThe resulting collection may share storage

Not every range should be described as "an array of values." A range is primarily a pair of bounds. Integer ranges can be iterated efficiently, but ranges over indices, such as String.Index, are often used to describe a slice boundary rather than to model standalone data.

swift
let values = ["A", "B", "C", "D"]
let middle = values[1..<3]

print(Array(middle))

Run compiles and executes on the server; output shows below.

The main interview distinction is: an Array stores elements; a Range describes an interval that can sometimes be iterated or used to slice another collection.

Why the choice matters

In iOS code, the choice is usually practical:

  • Use Array for ordered UI data: table rows, collection view items, search results, and navigation stacks.
  • Use Set for membership: selected IDs, deduped tags, visited nodes, enabled feature flags, and fast contains.
  • Use Dictionary for lookup by ID: cached models, sections by key, image tasks by URL, and metadata by identifier.
  • Use Range for paging, slicing arrays, validating numeric input, and working with collection indices.
  • Synchronize shared mutable collections when background callbacks, tasks, or queues can mutate them.

Interview angle

Walk the answer in this order: Swift collections are protocol-based -> Array is ordered random-access storage -> Set is unique Hashable membership -> Dictionary is Hashable key-value lookup -> Range represents an interval -> Big O is usually average-case for hash tables -> order for sets and dictionaries is unspecified -> value semantics and copy-on-write do not remove the need for synchronization around shared mutable collections.