Skip to Content
Course content

37: Sequence and Collection Protocols

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I was working on a project recently where I needed a custom wrapper around a set of data—essentially a Playlist object—and I realized I was doing a lot of tedious work. I found myself writing methods like getSong(at: Int) and allSongs() -> [Song] just to make the data accessible. It felt clunky. In Swift, if your object behaves like a list of things, it should be a list of things.

Why can't I just loop through this?

Let's start with a basic setup. I've got a Playlist struct that holds an array of song titles. Simple enough.

struct Playlist {
    var songs: [String]
}

let myPlaylist = Playlist(songs: ["Blue Monday", "Song 2", "Hysteria"])

// I want to do this:
for song in myPlaylist {
    print(song)
}

If you try to compile this, the compiler will scream at you. It'll say "Type 'Playlist' does not conform to protocol 'Sequence'". This is our first clue. The for-in loop doesn't actually care if your object is an Array; it only cares if your object conforms to the Sequence protocol.

Giving it a pulse with Sequence

The Sequence protocol is the most basic requirement for iteration. To make it work, I just need to provide a way to create an iterator. Now, I could write my own iterator from scratch, but since I'm already wrapping an array, I can just "cheat" and use the array's own iterator.

extension Playlist: Sequence {
    func makeIterator() -> IndexingIterator<[String]> {
        return songs.makeIterator()
    }
}

// Now this works!
for song in myPlaylist {
    print(song)
}


That's great for looping, but here is where I hit a wall. I tried to access a specific song using a subscript—something we do a thousand times a day with arrays—and it failed.

let secondSong = myPlaylist[1] // Error: Value of type 'Playlist' has no subscript

This is the fundamental distinction in Swift: a Sequence is just something you can move through from start to finish. It doesn't guarantee that you can go back, that you know how many items are in it, or that you can jump to a specific spot. It's like a stream of data; once you've iterated through it, it might be gone.

The frustration of missing indices

If I want my Playlist to feel like a first-class citizen in Swift—meaning I can use count, first, last, and subscripts—I need to move up to the Collection protocol. Collection inherits from Sequence, but it adds a lot of requirements. It demands that we define what an Index is and how to move between those indices.

I'll try to add Collection conformance now. This is where it gets slightly more verbose because we have to be explicit about the boundaries of our collection.

extension Playlist: Collection {
    // We tell Swift that our index is just an Integer
    typealias Index = Int

    var startIndex: Index {
        return songs.startIndex
    }

    var endIndex: Index {
        return songs.endIndex
    }

    func index(after i: Index) -> Index {
        return songs.index(after: i)
    }

    subscript(position: Index) -> String {
        return songs[position]
    }
}


Now, look at what happens. Because I've conformed to Collection, I didn't have to write a count property or a first property. Swift provides those for free based on the startIndex, endIndex, and the index(after:) method. I've essentially told Swift: "Here is how you start, here is how you stop, and here is how you get to the next item."

Putting it to the test

Let's see if this actually behaves the way I expect. I can now treat my custom struct as if it were an array, but I still maintain the encapsulation of the Playlist type.

let myPlaylist = Playlist(songs: ["Blue Monday", "Song 2", "Hysteria"])

print(myPlaylist.count)        // 3 (Automatic!)
print(myPlaylist[1])           // "Song 2" (Via our subscript)
print(myPlaylist.first!)       // "Blue Monday" (Automatic!)

for (index, song) in myPlaylist.enumerated() {
    print("\(index): \(song)")
}


The beauty of this approach is that any generic function in Swift that expects a Collection will now accept my Playlist. I'm no longer fighting the language; I'm using its protocols to define the behavior of my data.




📋 Practical Task

Implementing a CoordinateGrid Collection

You are building a game engine and have a CoordinateGrid struct that stores a flat array of tiles, but it represents a 2D grid. Instead of making the user manage a 1D array, you want the CoordinateGrid itself to be a Collection so it can be iterated over and accessed via subscripts.

Your Task:

  • Create a struct CoordinateGrid that contains an internal array of strings called tiles.
  • Implement the Collection protocol for CoordinateGrid.
  • Ensure that you define the Index, startIndex, endIndex, index(after:), and the subscript.
  • In your test code, instantiate a grid with 5 tiles, print the count of the grid, and use a for-in loop to print each tile.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.