Custom Subscripts in Swift
Learn how to extend your own types with subscripts, allowing you to index into them with simple syntax just like native arrays and dictionaries. By Michael Katz.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Comparing Subscripts, Properties and Functions
Subscripts are like computed properties in many regards:
- They consist of a getter and setter.
- The setter is optional, meaning a subscript can be either read-write or read-only.
- A read-only subscript doesn’t need an explicit
get
orset
block; the entire body is a getter. - In the setter, there’s a default parameter
newValue
with a type that equals the subscript’s return type. You typically only declare this parameter when you want to change its name to something other thannewValue
. - Users expect subscripts to be fast, preferably O(1), so keep them short and sweet!
The major difference with computed properties is that subscripts don’t have a property name, per se. Like operator overloading, subscripts let you override the language-level square brackets []
usually used for accessing elements of a collection.
Subscripts are also similar to functions in that they have a parameter list and return type, but they differ on the following points:
- Subscript parameters don’t have argument labels by default. If you want to use them, you’ll need to explicitly add them.
- Subscripts cannot use
inout
or default parameters. However, variadic (...
) parameters are allowed. - Subscripts cannot throw errors. This means a subscript getter must report errors through its return value and a subscript setter cannot throw or return any errors at all.
Adding a Second Subscript
There is one other point where subscripts are similar to functions: they can be overloaded. This means a type can have multiple subscripts, as long as they have different parameter lists or return types.
Add the following code after the existing subscript definition in Checkerboard
:
subscript(x: Int, y: Int) -> Square {
get {
return self[(x: x, y: y)]
}
set {
self[(x: x, y: y)] = newValue
}
}
This code adds a second subscript to Checkerboard
that accepts two integers rather than a Coordinate
tuple. Notice how you implement the second subscript using the first through self[(x: x, y: y)]
.
Try out this new subscript by adding the following lines to the end of the playground:
print(checkerboard[1, 2])
checkerboard[1, 2] = .white
print(checkerboard)
You’ll see the piece at (1, 2) change from red to white.
Using Dynamic Member Lookup
New in Swift 4.2 is the language feature of dynamic member lookup. This allows you to define runtime properties on a type. This means you can use the dot (.
) notation to index into a value or object, but you don’t have to define a specific property ahead of time.
This is most useful when your object has an internal data structure defined at runtime, like an object from a database or remote server. Or to put it another way, this brings key-value coding to Swift without needing an NSObject
subclass.
This feature requires two parts: A @dynamicMemberLookup
annotation and a special form of subscript
.
A Third Subscript
First, you’ll lay the foundation for dynamic lookup by introducing yet another subscript overload. This one uses a string to define the coordinate.
Add the following code below the previous subscript
definitions:
private func convert(string: String) -> Coordinate {
let expression = try! NSRegularExpression(pattern: "[xy](\\d+)")
let matches = expression
.matches(in: string,
options: [],
range: NSRange(string.startIndex..., in: string))
let xy = matches.map { String(string[Range($0.range(at: 1), in: string)!]) }
let x = Int(xy[0])!
let y = Int(xy[1])!
return (x: x, y: y)
}
subscript(input: String) -> Square {
get {
let coordinate = convert(string: input)
return self[coordinate]
}
set {
let coordinate = convert(string: input)
self[coordinate] = newValue
}
}
This code adds a few things:
- First,
convert(string:)
takes a string in the form of x#y# (where ‘#’ is a number) and returns aCoordinate
with an x-value and a y-value. You would normallythrow
an error if the regular expression pattern didn’t match but since subscripts can’t throw an error, there’s not much it can do except crash anyway, so thetry
is forced in this particular situation. - Then a newly-introduced subscript takes a string, converts it to a
Coordinate
, and reuses the first subscript defined earlier.
Try this one out by adding the following lines to the playground:
print(checkerboard["x2y5"])
checkerboard["x2y5"] = .red
print(checkerboard)
This time, one of the whites in the 6th row will turn red.
▪️🔴▪️🔴▪️🔴▪️🔴 🔴▪️🔴▪️🔴▪️🔴▪️ ▪️⚪️▪️⚪️▪️🔴▪️🔴 ▪️▪️▪️▪️▪️▪️▪️▪️ ▪️▪️▪️▪️▪️▪️▪️▪️ ⚪️▪️🔴▪️⚪️▪️⚪️▪️ ▪️⚪️▪️⚪️▪️⚪️▪️⚪️ ⚪️▪️⚪️▪️⚪️▪️⚪️▪️
Implementing Dynamic Member Lookup
So far this isn’t dynamic member lookup, it’s just a string index in a special format. Next you’ll sprinkle on the syntactic sugar.
First, add the following line at the top of the playground, immediately before the struct
keyword.
@dynamicMemberLookup
Then add the following beneath the other subscript
definitions:
subscript(dynamicMember input: String) -> Square {
get {
let coordinate = convert(string: input)
return self[coordinate]
}
set {
let coordinate = convert(string: input)
self[coordinate] = newValue
}
}
This is the same as the last subscript, but it has a special argument label: dynamicMember. This subscript
signature plus the annotation on the type allows you to access a Checkerboard
using the dot-syntax.
Wow, now the string index doesn’t need to be inside square brackets ([]
). You can access the string directly on the instance!
See it in action by adding these final lines to the bottom of the playground:
print(checkerboard.x6y7)
checkerboard.x6y7 = .red
print(checkerboard)
Run the playground again, and the last white piece will flip to red.
▪️🔴▪️🔴▪️🔴▪️🔴 🔴▪️🔴▪️🔴▪️🔴▪️ ▪️⚪️▪️⚪️▪️🔴▪️🔴 ▪️▪️▪️▪️▪️▪️▪️▪️ ▪️▪️▪️▪️▪️▪️▪️▪️ ⚪️▪️🔴▪️⚪️▪️⚪️▪️ ▪️⚪️▪️⚪️▪️⚪️▪️⚪️ ⚪️▪️⚪️▪️⚪️▪️🔴▪️
A Word of Warning
Dynamic lookup is a powerful feature that will make code a lot cleaner, especially server or scripting code. You no longer need to define an object’s structure at compile time to get dot-notation access.
Yet there are some dangerous drawbacks.
For example, the @dynamicMemberLookup
annotation basically tells the compiler not to check the validity of property names. You’ll still get type checking as well as completion for explicitly-defined properties, but now you can put anything after the period and the compiler won’t complain. You’ll only find out at runtime if you make a typo.
If you add this line to the playground, you won’t get an error until you run it.
checkerboard.queen