Chapter 70: Swift Collection Protocols
1. Why does Swift have Collection Protocols? (the most important big picture)
Swift wants to let you write generic, reusable code that works with many different kinds of collections — not just Array.
Examples of different kinds of collections:
- Array – ordered, allows duplicates
- Set – unordered, no duplicates
- Dictionary – key-value pairs (keys are unique)
- String – sequence of characters
- ArraySlice – view into part of an array
- Custom collections you write yourself
- Third-party types (e.g. from libraries)
Instead of writing separate functions for each type:
|
0 1 2 3 4 5 6 7 8 |
func printAll(array: [String]) { … } func printAll(set: Set<String>) { … } func printAll(dictKeys: Dictionary<String, Int>.Keys) { … } |
Swift lets you write one generic function that works with all of them — as long as they conform to the right protocol.
That is the whole reason for Collection protocols.
2. The Main Collection Protocols – Hierarchy Overview
Here is the most important family tree you should keep in mind:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
Sequence ↑ Collection ← most important for everyday work ↑ BidirectionalCollection ↑ RandomAccessCollection ← Array, ArraySlice, ContiguousArray belong here |
You will mostly interact with these four:
| Protocol | Ordered? | Index type | Random access? | Typical types that conform | When you care about it |
|---|---|---|---|---|---|
| Sequence | Yes | — (no index) | No | Array, Set, Dictionary, String, Range, StrideThrough… | When you just iterate (for-in, forEach, map, filter…) |
| Collection | Yes | Index (opaque) | No (but fast in practice for Array) | Array, Set, Dictionary, String, ArraySlice… | When you need .count, .isEmpty, subscript [index], .first, .last… |
| BidirectionalCollection | Yes | Supports going backward | No | Array, Set, Dictionary, String… | When you need .last, .reversed(), iterate backward |
| RandomAccessCollection | Yes | Supports fast jumping | Yes | Array, ArraySlice, ContiguousArray, Range… | When you need fast random access coll[i] or slicing |
Rule of thumb used by most good Swift developers:
- If you only iterate (for-in, map, filter, reduce, forEach) → Sequence is enough
- If you need count, subscript, first/last → you usually want Collection
- If you need fast random access or slicing → RandomAccessCollection (Array is this)
3. Sequence – the foundation (what almost everything conforms to)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
func printAllElements<S: Sequence>(_ items: S) where S.Element == String { for item in items { print(item) } } let names = ["Rahul", "Priya", "Aarav"] let uniqueNames = Set(names) let nameString = "RahulPriyaAarav" printAllElements(names) // Array printAllElements(uniqueNames) // Set printAllElements(nameString) // String |
Real-life use-case – generic logging function
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
func logItems<S: Sequence>(_ items: S, prefix: String = "→") where S.Element: CustomStringConvertible { items.forEach { item in print("\(prefix) \(item)") } } logItems([1, 2, 3]) // → 1 → 2 → 3 logItems(Set(["apple", "banana"])) // → apple → banana (order not guaranteed) logItems("hello".map { $0 }) // → h → e → l → l → o |
4. Collection – when you need count, indices, subscript
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
func printWithIndices<C: Collection>(_ items: C) where C.Element: CustomStringConvertible { if items.isEmpty { print("Empty collection") return } print("Collection with \(items.count) items:") for (offset, element) in items.enumerated() { print(" [\(offset)] \(element)") } } |
This function works with Array, Set, Dictionary.keys, String, ArraySlice, etc.
Real-life example – safe slicing / pagination
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
func getPage<C: Collection>(_ items: C, page: Int, pageSize: Int = 10) -> C.SubSequence where C.Index == Int { let start = page * pageSize let end = min(start + pageSize, items.count) guard start < items.count else { return items[items.endIndex..<items.endIndex] // empty slice } return items[start..<end] } |
5. RandomAccessCollection – when you need fast random access
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
func swapFirstAndLast<C: RandomAccessCollection & MutableCollection>(_ collection: inout C) { guard !collection.isEmpty else { return } // Because it's RandomAccessCollection, subscript is O(1) let firstIndex = collection.startIndex let lastIndex = collection.index(before: collection.endIndex) collection.swapAt(firstIndex, lastIndex) } |
|
0 1 2 3 4 5 6 7 8 |
var numbers = [10, 20, 30, 40, 50] swapFirstAndLast(&numbers) print(numbers) // [50, 20, 30, 40, 10] |
Array, ArraySlice, ContiguousArray conform to RandomAccessCollection. Set and Dictionary do not — their access is fast but not guaranteed O(1) for indexed access.
6. Real-life examples you will actually write
Example 1 – Generic function that works on any Collection
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
func printSummary<C: Collection>(_ items: C) where C.Element: CustomStringConvertible { if items.isEmpty { print("No items") return } print("Showing \(items.count) items:") // Safe because Collection guarantees .first / .last if let first = items.first, let last = items.last { print(" First: \(first)") print(" Last: \(last)") } // enumerated() works because Collection has indices items.enumerated().forEach { index, item in print(" #\(index + 1): \(item)") } } |
This one function works with Array, Set, Dictionary.keys, String, etc.
Example 2 – Safe pagination / slicing
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
func getPage<C: Collection>(_ items: C, page: Int, pageSize: Int = 20) -> C.SubSequence where C.Index == Int { let start = page * pageSize guard start < items.count else { return items[items.endIndex..<items.endIndex] // empty } let end = min(start + pageSize, items.count) return items[start..<end] } |
7. Quick Summary – Which protocol should you usually ask for?
| You need… | Minimum protocol you should ask for | Why? / Typical generic signature |
|---|---|---|
| Just iterate (for-in, forEach, map, filter) | Sequence | func process<S: Sequence>(_ items: S) where S.Element == String |
| Need .count, .isEmpty, .first, .last, subscript | Collection | func summary<C: Collection>(_ items: C) |
| Need fast random access / slicing | RandomAccessCollection | func swapFirstLast<C: RandomAccessCollection & MutableCollection>(_ c: inout C) |
| Need bidirectional walking (rare) | BidirectionalCollection | reversed(), backward iteration |
Rule of thumb used by most good Swift developers:
- Start with Sequence — it’s the most flexible
- Move to Collection when you need .count, indices, subscript
- Only ask for RandomAccessCollection when you really need fast random access or slicing
8. Small Practice – Try these
- Write a generic function that prints the first and last element of anyCollection (use Collection constraint)
- Write a generic function that returns the number of elements and the first element (if exists) of any Sequence
- Write a function that takes any RandomAccessCollection and swaps the first and last element (only if there are at least 2 elements)
Paste your code here if you want feedback or want to see cleaner versions!
What would you like to explore next?
- Sequence vs Collection vs RandomAccessCollection in depth
- Writing your own custom collection type
- Collection protocols in SwiftUI (ForEach, List, data sources)
- Lazy collections (lazy, LazySequence, LazyMapCollection…)
- Or move to another topic (optionals, switch, functions, mutability…)
Just tell me — we’ll continue in the same clear, patient, detailed style 😊
