Swift’s standard library already gives us strong collection primitives. Yet many developers still write nested loops, temporary buffers, and index math that is harder to read and easier to break.
Apple’s open-source swift-algorithms package fills that gap with focused sequence and collection APIs designed for correctness, clarity, and predictable performance.
Many Swift developers still do not know this package exists, even though it is maintained by Apple and widely used in production Swift code.
In this article, we will learn Swift Algorithms in a practical way and cover these operations:
Swift Algorithms is an incubation package for collection and sequence operations that may later move into the Swift standard library. That means these APIs are explored with real-world feedback first.
In day-to-day development, this package gives three big benefits:
Add the package to your Package.swift:
.package(url: "https://github.com/apple/swift-algorithms", from: "1.2.1"),Then add the product to your target dependencies:
.target(
name: "AppTarget",
dependencies: [
.product(name: "Algorithms", package: "swift-algorithms")
]
)And import it in Swift files where needed:
import AlgorithmsUse combinations(ofCount:) when you need groups and order inside a group does not matter.
The combinations(ofCount:) method returns every different combination of size k.
Each combination keeps the same element order as the original collection.
If a value appears multiple times in the source, each position is treated as a separate element. So repeated looking combinations can appear.
import Algorithms
let users = ["Asha", "Ben", "Chloe", "Diego"]
let pairs = users.combinations(ofCount: 2)
for pair in pairs {
print(pair)
}Output:
["Asha", "Ben"]
["Asha", "Chloe"]
["Asha", "Diego"]
["Ben", "Chloe"]
["Ben", "Diego"]
["Chloe", "Diego"]You can also pass a range like 2...3 to generate combinations of multiple sizes.
import Algorithms
let numbers = [10, 20, 30, 40]
for combo in numbers.combinations(ofCount: 2...3) {
print(combo)
}Output:
[10, 20]
[10, 30]
[10, 40]
[20, 30]
[20, 40]
[30, 40]
[10, 20, 30]
[10, 20, 40]
[10, 30, 40]
[20, 30, 40]Good use cases:
Important behavior:
Use permutations() when order matters.
The permutations() method returns all full permutations.
permutations(ofCount:) returns partial permutations of length k.
Results are produced in lexicographic order based on original element positions.
If the source contains repeated values, they are treated as separate positions. So duplicates can appear.
Use uniquePermutations() when you want only unique outputs.
import Algorithms
let steps = ["Login", "Verify", "Checkout"]
for flow in steps.permutations() {
print(flow)
}Output:
["Login", "Verify", "Checkout"]
["Login", "Checkout", "Verify"]
["Verify", "Login", "Checkout"]
["Verify", "Checkout", "Login"]
["Checkout", "Login", "Verify"]
["Checkout", "Verify", "Login"]Need partial orderings? Use permutations(ofCount:):
for flow in steps.permutations(ofCount: 2) {
print(flow)
}Output:
["Login", "Verify"]
["Login", "Checkout"]
["Verify", "Login"]
["Verify", "Checkout"]
["Checkout", "Login"]
["Checkout", "Verify"]If source values may repeat and you only want unique outputs, use uniquePermutations():
let values = [1, 1, 2]
for p in values.uniquePermutations() {
print(p)
}Output:
[1, 1, 2]
[1, 2, 1]
[2, 1, 1]Use product(_:_:) for Cartesian products across two inputs.
It returns every pair made from the first sequence and the second collection.
In simple terms, it behaves like a nested loop: for each element in the first input, iterate all elements in the second input.
The second input must be a Collection because it is traversed many times.
If either input is empty, the result is empty.
import Algorithms
let regions = ["US", "EU", "IN"]
let plans = ["Free", "Pro"]
for (region, plan) in product(regions, plans) {
print("\(region)-\(plan)")
}Output:
US-Free
US-Pro
EU-Free
EU-Pro
IN-Free
IN-ProYou can write nested loops for this, but product is easier to read and reuse.
Chunking APIs split a collection into consecutive, non-overlapping subsequences.
Unlike split, chunking keeps every original element.
That means joining all chunks gives you back the same collection.
chunked(by:) groups by a relationship between neighboring elements.
chunked(on:) groups by equal projected keys.
chunks(ofCount:) creates fixed-size chunks.
evenlyChunked(in:) creates a fixed number of near-equal chunks.
chunked(by:) - split by adjacent relationshipimport Algorithms
let readings = [2, 4, 5, 1, 3, 7, 2]
let ascendingRuns = readings.chunked(by: { $0 <= $1 })
print(ascendingRuns.map { Array($0) })Output:
[[2, 4, 5], [1, 3, 7], [2]]chunked(on:) - split by projected key changeslet names = ["Ava", "Amy", "Brian", "Bella", "Chris"]
let groups = names.chunked(on: { $0.first! })
print(groups.map { ($0.0, Array($0.1)) })Output:
[("A", ["Ava", "Amy"]), ("B", ["Brian", "Bella"]), ("C", ["Chris"])]chunks(ofCount:) - fixed-size windowslet jobs = Array(1...10)
let batches = jobs.chunks(ofCount: 3)
print(batches.map { Array($0) })Output:
[[1, 2, 3], [4, 5, 6], [7, 8, 9], [10]]evenlyChunked(in:) - split into N near-even partslet allItems = Array(0..<15)
let shards = allItems.evenlyChunked(in: 4)
print(shards.map { Array($0) })Output:
[[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11], [12, 13, 14]]Practical use cases:
Use chain(_:_:) to concatenate two sequences of the same element type without allocating intermediate storage.
It yields every element of the first sequence.
Then it yields every element of the second sequence.
Compared with building arrays and calling joined(), chain avoids unnecessary allocation.
It also works naturally with different underlying sequence types.
import Algorithms
let top = [101, 102, 103]
let rest = 200...202
let allIDs = chain(top, rest)
print(Array(allIDs))Output:
[101, 102, 103, 200, 201, 202]Why not [top, Array(rest)].joined()?
chain supports heterogeneous sequence types.Cycle APIs repeat a collection’s elements in original order.
Use cycled() for repeating a collection forever, producing an infinite sequence, and cycled(times:) for bounded repetition.
The infinite form is intentionally sequence-only.
An infinite value cannot behave like a practical finite collection.
import Algorithms
for value in ["Red", "Blue"].cycled(times: 3) {
print(value)
}Output:
Red
Blue
Red
Blue
Red
BlueCommon patterns:
Returns a sequence with only the unique elements of this sequence, in the order of the first occurrence of each unique element.
Use uniqued() to remove duplicate elements while preserving first-seen order.
uniqued() uses the element itself as the uniqueness key (for Hashable elements).
uniqued(on:) uses a projected key, such as \.id.
In both cases, the first time a key appears is kept.
Later repeats are dropped.
The relative order of kept elements stays the same.
import Algorithms
let tags = ["swift", "ios", "swift", "performance", "ios"]
let deduped = Array(tags.uniqued())
print(deduped)Output:
["swift", "ios", "performance"]Use uniqued(on:) for projection based uniqueness:
struct User {
let id: Int
let email: String
}
let users = [
User(id: 1, email: "a@example.com"),
User(id: 2, email: "b@example.com"),
User(id: 1, email: "a2@example.com")
]
let uniqueUsers = users.uniqued(on: \.id)
print(uniqueUsers.map(\.email))Output:
["a@example.com", "b@example.com"]Randomly selects the specified number of elements from given collection.
randomSample(count:) returns sampled elements in random order.
randomStableSample(count:) returns sampled elements in the same relative order as the source collection.
If count is greater than the collection size, all elements are returned.
If you need reproducible behavior in tests, use overloads that accept a custom random-number generator.
import Algorithms
let source = Array(1...100)
let sample = source.randomSample(count: 5)
print(sample)Output (example, changes every run):
[44, 27, 97, 58, 37]Need order preserved relative to source? Use randomStableSample(count:):
let stable = source.randomStableSample(count: 5)
print(stable)Output (example, preserves source order):
[28, 33, 44, 62, 75]To control randomness explicitly, pass your own RandomNumberGenerator:
var rng = SystemRandomNumberGenerator()
let controlledSample = source.randomSample(count: 5, using: &rng)
print(controlledSample)Output:
[28, 24, 26, 87, 5] //Returns 5 sampled elements using the RandomNumberGenerator you pass in.The indexed() method is similar to the standard library’s enumerated() method, but provides the index with each element instead of a zero-based counter.
It returns pairs of (index, element).
Here, index is the actual index type of the collection, not an auto-incrementing Int counter.
This is important for slices and custom collections where true indices carry semantic meaning.
import Algorithms
let array = ["a", "b", "c", "d"]
let slice = array[1...2] // ["b", "c"]
for (index, value) in slice.indexed() {
print(index, value)
}Output:
1 b
2 cenumerated() always uses an Int counter starting from zero. indexed() pairs each element with its true collection index, which matters for non-zero-based and custom index collections.
Partition APIs help split data into two groups using a predicate.
The standard library already has partition(by:), but it is not stable.
Swift Algorithms adds stable partitioning, subrange partitioning, and partitioningIndex(where:) to locate the split index in already partitioned data.
It also adds partitioned(by:) for a non-mutating tuple of both groups.
stablePartition(by:) for mutable collectionsimport Algorithms
var numbers = [10, 20, 30, 40, 50]
let pivot = numbers.stablePartition(by: { $0.isMultiple(of: 20) })
// left side: elements where predicate is false (original order preserved)
// right side: elements where predicate is true (original order preserved)
print(pivot)
print(numbers)Output:
3
[10, 30, 50, 20, 40]partitioningIndex(where:) for already partitioned collectionslet prepartitioned = [1, 3, 5, 2, 4, 6]
let split = prepartitioned.partitioningIndex(where: { $0.isMultiple(of: 2) })
print(split)Output:
3partitioned(by:) for non-mutating tuple outputlet words = ["cat", "elephant", "dog", "giraffe"]
let (short, long) = words.partitioned(by: { $0.count > 3 })
print(short)
print(long)Output:
["cat", "dog"]
["elephant", "giraffe"]Use rotate(toStartAt:) to mutate a collection so a chosen index becomes the new start.
rotate(toStartAt:) rotates an entire mutable collection.
rotate(subrange:toStartAt:) rotates only a specific range.
This is useful for in-place reordering and divide-and-conquer algorithms.
Each call returns the index where the old start element moved.
import Algorithms
var queue = [10, 20, 30, 40, 50]
let newIndex = queue.rotate(toStartAt: 2)
// queue == [30, 40, 50, 10, 20]
print(newIndex)
print(queue)Output:
3
[30, 40, 50, 10, 20]Range-based rotation is useful in divide-and-conquer style mutations:
var queue = [10, 20, 30, 40, 50]
let newIndex = queue.rotate(toStartAt: 2)
// queue == [30, 40, 50, 10, 20]
queue.rotate(subrange: 0..<3, toStartAt: 1)
print(queue)Output:
[40, 50, 30, 10, 20]Use standard library APIs when the operation is simple and already expressive:
map, filter, reducesplit when separators should be removedenumerated when a plain counter is enoughUse Swift Algorithms when a dedicated API makes your intent clearer:
ofCount early.Collection for repeated traversal or count access.indexed() for nontrivial collections where offsets are not true indices.Use this checklist when introducing Swift Algorithms in production code:
stable vs unstable results).Swift Algorithms does not replace the standard library. It extends it with focused APIs for common collection problems.
If your project works heavily with sequences and collections, this package quickly improves readability and reduces loop-level bugs.
Official resources:
If you have suggestions, feel free to connect with me on X and send me a DM. If this article helped you, Buy me a coffee.