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.

Version

  • Swift 4.2, iOS 12, Xcode 10
Update note: Michael Katz updated this tutorial for Swift 4.2. Evan Dekhayser wrote the original and Mikael Konutgan made a previous update.

Custom subscripts are a powerful language feature that enhance the convenience factor and readability of your code.

Like operator overloading, custom subscripts let you use native Swift constructs. You can use something concise like checkerBoard[2][3] rather than the more verbose checkerBoard.objectAt(x: 2, y: 3).

In this tutorial, you’ll explore custom subscripts by building the foundations for a basic checkers game in a playground. You’ll see how easy it is to use subscripting to move pieces around the board. When you’re done, you’ll be well on your way to building a new game to keep your fingers occupied during all your spare time.

Oh, and you’ll know a lot more about subscripts too! :]

Note: This tutorial assumes you already know the basics of Swift development. If you are new to Swift, check out some of our beginner Swift tutorials or read the Swift Apprentice first.

Getting Started

To start, create a new playground. Go to File ▸ New ▸ Playground…, choose the iOS ▸ Blank template and click Next. Name the file Subscripts.playground and click Create.

Replace the default text with:

import Foundation

struct Checkerboard {
  enum Square: String {
    case empty = "▪️"
    case red = "🔴"
    case white = "⚪️"
  }

  typealias Coordinate = (x: Int, y: Int)

  private var squares: [[Square]] = [
    [ .empty, .red,   .empty, .red,   .empty, .red,   .empty, .red   ],
    [ .red,   .empty, .red,   .empty, .red,   .empty, .red,   .empty ],
    [ .empty, .red,   .empty, .red,   .empty, .red,   .empty, .red   ],
    [ .empty, .empty, .empty, .empty, .empty, .empty, .empty, .empty ],
    [ .empty, .empty, .empty, .empty, .empty, .empty, .empty, .empty ],
    [ .white, .empty, .white, .empty, .white, .empty, .white, .empty ],
    [ .empty, .white, .empty, .white, .empty, .white, .empty, .white ],
    [ .white, .empty, .white, .empty, .white, .empty, .white, .empty ]
  ]
}

Checkerboard contains three definitions:

  • Square represents the state of an individual square on the board. .empty represents an empty square while .red and .white represent the presence of a red or white piece on that square.
  • Coordinate is an alias for a tuple of two integers. You’ll use this type to access the squares on the board.
  • squares is the two-dimensional array that stores the state of the board.

Next, add:

extension Checkerboard: CustomStringConvertible {
  var description: String {
    return squares.map { row in row.map { $0.rawValue }.joined(separator: "") }
        .joined(separator: "\n") + "\n"
  }
}

This is an extension to add CustomStringConvertible conformance. With a custom description, you can print a checkerboard to the console.

Open the console using View ▸ Debug Area ▸ Show Debug Area, then enter the following lines at the bottom of the playground:

var checkerboard = Checkerboard()
print(checkerboard)

This code initializes an instance of Checkerboard. Then it prints the description property to the console using the CustomStringConvertible implementation.

After pressing the Execute Playground button, the output in your console should look like this:

▪️🔴▪️🔴▪️🔴▪️🔴
🔴▪️🔴▪️🔴▪️🔴▪️
▪️🔴▪️🔴▪️🔴▪️🔴
▪️▪️▪️▪️▪️▪️▪️▪️
▪️▪️▪️▪️▪️▪️▪️▪️
⚪️▪️⚪️▪️⚪️▪️⚪️▪️
▪️⚪️▪️⚪️▪️⚪️▪️⚪️
⚪️▪️⚪️▪️⚪️▪️⚪️▪️

Getting and Setting Pieces

Looking at the console, it’s pretty easy for you to know what piece occupies a given square, but your program doesn’t have that power yet. It can’t know which player is at a specified coordinate because the squares array is private.

There’s an important point to make here: The squares array is the implementation of the the board. However, the user of a Checkerboard shouldn’t know anything about the implementation of that type.

A type should shield its users from its internal implementation details; that’s why the squares array is private.

With that in mind, you’re going to add two methods to Checkerboard to find and change a piece at a given coordinate.

Add the following methods to Checkerboard, after the spot where you assign the squares array:

func piece(at coordinate: Coordinate) -> Square {
  return squares[coordinate.y][coordinate.x]
}

mutating func setPiece(at coordinate: Coordinate, to newValue: Square) {
  squares[coordinate.y][coordinate.x] = newValue
}

Notice how you access squares – using a Coordinate tuple – rather than accessing the array directly. The actual storage mechanism is an array-of-arrays. That is exactly the kind of implementation detail you should shield the user from!

Defining Custom Subscripts

You may have noticed that these methods look an awful lot like a property getter and setter combination. Maybe you should implement them as a computed property instead?

Unfortunately, that won’t work. These methods require a Coordinate parameter, and computed properties can’t have parameters. Does that mean you’re stuck with methods?

Well, no – this special case is exactly what subscripts are for! :]

Look at how you define a subscript:

subscript(parameterList) -> ReturnType {
  get {
    // return someValue of ReturnType
  }

  set (newValue) {
    // set someValue of ReturnType to newValue
  }
}

Subscript definitions mix both function and computed property definition syntax:

  • The first part looks a lot like a function definition, with a parameter list and a return type. Instead of the func keyword and a function name, you use the special subscript keyword.
  • The main body looks a lot like a computed property with a getter and a setter.

The combination of function and property syntax highlights the power of subscripts. It provides a shortcut for accessing the elements of an indexed collection. You’ll learn more about that soon but, first, consider the following example.

Replace piece(at:) and setPiece(at:to:) with the following subscript:

subscript(coordinate: Coordinate) -> Square {
  get {
    return squares[coordinate.y][coordinate.x]
  }
  set {
    squares[coordinate.y][coordinate.x] = newValue
  }
}

You implement the getter and setter of this subscript exactly the same way as you implement the methods they replace:

  • Given a Coordinate, the getter returns the square at the column and row.
  • Given a Coordinate and value, the setter accesses the square at the column and row and replaces its value.

Give your new subscript a test drive by adding the following code to the end of the playground:

let coordinate = (x: 3, y: 2)
print(checkerboard[coordinate])
checkerboard[coordinate] = .white
print(checkerboard)

The playground will tell you the piece at (3, 2) is red. After changing it to white, the output in the console will be:

▪️🔴▪️🔴▪️🔴▪️🔴
🔴▪️🔴▪️🔴▪️🔴▪️
▪️🔴▪️⚪️▪️🔴▪️🔴
▪️▪️▪️▪️▪️▪️▪️▪️
▪️▪️▪️▪️▪️▪️▪️▪️
⚪️▪️⚪️▪️⚪️▪️⚪️▪️
▪️⚪️▪️⚪️▪️⚪️▪️⚪️
⚪️▪️⚪️▪️⚪️▪️⚪️▪️

You can now find out which piece is at a given coordinate, and set it, by using checkerboard[coordinate] in both cases. A shortcut indeed!

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 or set 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 than newValue.
  • Users expect subscripts to be fast, preferably O(1), so keep them short and sweet!

Subscripts are just computed properties in disguise.

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:

  1. First, convert(string:) takes a string in the form of x#y# (where ‘#’ is a number) and returns a Coordinate with an x-value and a y-value. You would normally throw 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 the try is forced in this particular situation.
  2. 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

Where to Go From Here?

You can download the final playground using the Download Materials button at the top or bottom of this tutorial.

Now that you’ve added subscripts to your toolkit, look for opportunities to use them in your own code. When used properly, they make your code more readable and intuitive.

That said, you don’t always want to revert to subscripts. If you’re writing an API, your users are used to using subscripts to access elements of an indexed collection. Using them for other things will feel unnatural and forced.

For further information on subscripts, check out this chapter of The Swift Programming Language documentation by Apple.

If you have any questions or comments, please leave them below!

Add a rating for this content

Contributors

Comments