Swift Algorithms - Apple’s Hidden Collection and Sequence APIs You Should Be Using

March 21, 2026

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:

  • Combinations
  • Permutations
  • Product
  • Chunked
  • Chain
  • Cycle
  • Unique
  • Random Sampling
  • Indexed
  • Partition
  • Rotate

Why Swift Algorithms Matters

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:

  • Readability: intent-focused operations replace handwritten control flow.
  • Correctness: less custom state management means fewer logic traps.
  • Performance transparency: most APIs document their complexity and constraints clearly.

Add Swift Algorithms to Your Project

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 Algorithms

1. Combinations

Use 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:

  • Pair-programming assignments
  • Team subgroup generation
  • Feature flag test matrices where order does not matter

Important behavior:

  • Results follow lexicographic order based on the original collection order.
  • Duplicate values in the source are treated as distinct positions.

2. Permutations

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]

3. Product

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-Pro

You can write nested loops for this, but product is easier to read and reuse.


4. Chunked

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 relationship

import 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 changes

let 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 windows

let 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 parts

let 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:

  • Pagination and worker batching
  • Grouping telemetry by key transitions
  • Breaking large datasets for parallel processing

5. Chain

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.
  • You avoid extra materialization work.
  • Conditional collection conformances are preserved when possible.

6. Cycle

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
Blue

Common patterns:

  • Repeating UI states in previews
  • Round-robin assignment simulation
  • Pattern generation in deterministic test fixtures

7. Unique

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"]

8. Random Sampling

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.

9. Indexed

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 c

enumerated() 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.


10. Partition

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 collections

import 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 collections

let prepartitioned = [1, 3, 5, 2, 4, 6]
let split = prepartitioned.partitioningIndex(where: { $0.isMultiple(of: 2) })
print(split)

Output:

3

partitioned(by:) for non-mutating tuple output

let words = ["cat", "elephant", "dog", "giraffe"]
let (short, long) = words.partitioned(by: { $0.count > 3 })
print(short)
print(long)

Output:

["cat", "dog"]
["elephant", "giraffe"]

11. Rotate

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]

Standard Library vs Swift Algorithms: When to Choose What

Use standard library APIs when the operation is simple and already expressive:

  • map, filter, reduce
  • split when separators should be removed
  • enumerated when a plain counter is enough

Use Swift Algorithms when a dedicated API makes your intent clearer:

  • You need combinations or permutations
  • You need stable/unstable sampling semantics
  • You need true indices, stable partitioning, or in-place rotation helpers
  • You want to replace non-obvious loop machinery with named algorithms

Common Pitfalls and How to Avoid Them

  • Combinatorial explosion: permutations and combinations grow rapidly. Limit ofCount early.
  • Sequence vs collection assumptions: some APIs require Collection for repeated traversal or count access.
  • Stable vs unstable semantics: choose sampling/partitioning methods deliberately based on ordering needs.
  • Index correctness: use indexed() for nontrivial collections where offsets are not true indices.

A Practical Adoption Checklist

Use this checklist when introducing Swift Algorithms in production code:

  1. Start with one module that has algorithm-heavy loops.
  2. Replace custom loops with named APIs only where readability improves.
  3. Keep performance-sensitive paths benchmarked before and after.
  4. Add tests for ordering-sensitive behavior (stable vs unstable results).
  5. Document team conventions for when to prefer package APIs over hand-written loops.

Final Thoughts

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.