We’re gonna take a break from talking about ReaktiveKit and Bond this week. I went down kind of a rabbit hole there with building our own Event and Observable classes, so this week we’re gonna go off topic a bit and talk about subscripting in Swift.
We use subscripts in Swift all the time, usually in an array or a dictionary. They look like this:
someArray[2] = "Some value"someDictionary["some key"] = "Some value
You get the idea. We do all kinds of stuff with arrays and dictionaries. But what’s rad, is that we can add subscripting to our own custom classes and structs. The same way we can provide an initializer for our custom types, we can provide a subscript. Here’s a very simple example:
struct Group {
subscript(index: Int) -> Int {get {return index}}}
let justiceLeague = Group()print(justiceLeague[3]) //prints "3"print(justiceLeague[2]) //prints "2"
That’s the very basics of how this works. Let’s ramp this up a bit and show how being able to subscript can be useful. In an app I published a little while back, I have a grid that is used to represent musical notes:
Each button represents a note, which in turn is represented by a struct. We’ll call that struct ‘Note.’ Here’s a simple implementation of Note:
struct Note {let pitch: Intlet isOn: Bool
static let empty = Note(pitch: 0, isOn: false)}
Now what would be awesome is if there was a way to represent the grid of notes in a way that could be subscripted. So anytime we want to change a note on this grid, we could do something like this:
grid[2, 3] = Note(pitch: 67, isOn: true)
Here’s what a custom class that can represent our grid might look like:
struct NoteMatrix{let rows: Intlet columns: Intvar grid = [Note]()
init(rows: Int, columns: Int) {self.rows = rowsself.columns = columnsgrid = Array(repeating: Note.empty, count: columns * rows)}
subscript(row: Int, column: Int) -> Note {get {return grid[(row * columns) + column]}set(newValue) {grid[(row * columns) + column] = newValue}}}
Now we can do this:
var score = NoteMatrix(rows: 4, columns: 4)score[2, 3] = Note(pitch: 67, isOn: true)
print(score[2, 3].pitch) //prints "67"
Hey, wouldya look at that. We can also add an assert statement in the “get-set” part of our subscript that will crash everything if we reach outside of the number of rows and columns:
assert(row >= 0 && row < rows && column >= 0 && column < columns, "Index out of range")
Essentially we have the building blocks for creating collection types. I wanted to see if I could make this Matrix class work with generics, so I kind of hacked together something that… well it functions without any errors. It’s not pretty though. If you have some better ways to go about this, definitely let me know in the comments sections. Here it… ugh… here it is:
struct Matrix<T>{let rows: Intlet columns: Intvar grid = [Int: [Int: T]]()
init(rows: Int, columns: Int) {self.rows = rowsself.columns = columns}
subscript(row: Int, column: Int) -> T? {get {assert(indexIsValid(row: row, column: column), "Index out of range")if let gridRow = grid[row] {if let output = gridRow[column] {return output} else {return nil}}else {return nil}}
set(newValue) {assert(indexIsValid(row: row, column: column), "Index out of range")if let newValue = newValue {grid[row] = [column: newValue]}}}
func indexIsValid(row: Int, column: Int) -> Bool {return row >= 0 && row < rows && column >= 0 && column < columns}
}
struct Note {let pitch: Intlet isOn: Bool}
var noteGrid = Matrix<Note>(rows: 4, columns: 4)
noteGrid[1, 1] = Note(pitch: 67, isOn: true)
print(noteGrid[1, 1]?.pitch) //prints "67"
That’s gross. It definitely needs to be reworked. But now we have a generic Matrix that we can use with any custom type of ours. Seriously, if you have a better way to go about this, let me know. I have a feeling that to have a way to store values in a custom collection type, I have to go a bit further under the hood. But, anyway. Now you can see a bit how subscripting works!