Subscripting first came to Objective-C in Xcode 4.4, way back in the middle of 2012. At this time, Swift was already two years into development. As with many other powerful features in Objective C, such as ARC, subscripting also became part of Swift.
Subscripts don’t actually add functionality to a class, instead they’re shortcuts to access members of a collection. It sounds like a trivial application, but subscripts significantly enhance the convenience factor and readability of your code.
For example, it used to be that you would write myArray.objectAtIndex(2)
to access the third element of the array myArray
. As of Xcode 4.4, you can access the third element with the much simpler myArray[2]
.
Moreover, dictionary access is simpler as well; instead of value = [myDictionary objectForKey:@"VotingAge"]
, you can now write value = myDictionary[@"VotingAge"]
Check out iOS 6 by Tutorials (Second edition) for the details on this and all the other advances in modern Objective-C.
Moreover, Objective-C supports adding subscripts to custom classes, but it requires some effort. In contrast, Swift makes adding subscripts to your own classes easier to implement than ever. Yes, that’s right, your life is about to get easier, compliments of Apple.
Getting Started
In this Swift tutorial, you’re going to explore subscripts by building a basic checkers game in a playground. You’ll see how easy moving pieces around is, illustrating the power of subscripting right on your game board.
When you’re done, you’ll also have a new game to keep your fingers occupied during all of your spare time.
Download the starter project. Look over the two types that already present in the playground:
enum PlayerColor
: Represents the color of a piece on the board. Note that a piece with PlayerColor type.None
represents a blank space on the board.class GameBoard
: This is the main class you’ll work with. It has a 2-dimensional array of PlayerColors that holds the board’s data.
GameBoard controls all the gameplay and keeps track of the piece locations, and to make it functional you’ll add subscripting to GameBoard
.
Open the Assistant Editor by going to View\Assistant Editor\Show Assistant Editor to view the console.
Enter the following lines at the bottom of the playground, after the closing brackets of the GameBoard class:
let board = GameBoard() board.displayBoard() |
After initializing the instance GameBoard
of the GameBoard class, the call to board.displayBoard()
prints the board to the console.
The board currently looks like this:
-r-r-r-r r-r-r-r- -r-r-r-r -------- -------- b-b-b-b- -b-b-b-b b-b-b-b- |
The board
array comprises eight elements, each of which is an array of eight elements. In effect, this makes board
a two-dimensional array. For example, board[2][5]
refers to the sixth element of the third subarray — remember that the first element of an array is at index 0).
Note how the board’s subscripts are y-before-x. No, you’re not looking through at in in one of those goofy mirrors at a carnival.
Though it seems backwards, it’s actually not on this project, because you have to access it in this way because of how board
is set up.
The following image should help clarify this concept. From the user’s perspective, the square at (7,0) is at the top-right corner. However, in the image, the square (7,0) appears in the first array of board
. The seventh element of this subarray is the piece at square (7,0).
Therefore, to locate the piece at square (7,0), you would need to access board[0][7]
.
Implementing a Simple Moving Method
An effective way to create subscripts is to write the code in functions first, then reuse it when you build the subscripts.
With that in mind, you’re going to implement a simple function that moves a piece from one location to another. The coordinates of the locations consist of a tuple of two Int’s: (Int,Int)
.
Add this function to the end of the GameBoard
class:
func movePieceFrom(from: (Int,Int), to: (Int,Int)) { let pieceToMove = board[from.1][from.0] board[from.1][from.0] = empty() board[to.1][to.0] = pieceToMove } |
This is a very basic implementation for now – you’ll add more game logic here later in the tutorial.
First, you store the piece you want to move in a variable. The next line replaces that piece in the GameBoard
with a blank space. Then, you move the piece where you want it to go.
Try this out. At the bottom of the file, add the following two lines:
board.movePieceFrom((2,5), to: (3,4)) board.displayBoard() |
Here, you tell your GameBoard
to move the piece at (2,5), which happens to be a black piece, diagonally to the right.
Look in the console, the board shows that the piece from (2,5) moved to its new position at (3,4):
-r-r-r-r r-r-r-r- -r-r-r-r -------- ---b---- b---b-b- -b-b-b-b b-b-b-b- |
Finding Who is Where
Looking at the console, it’s pretty easy for you to know what piece occupies a given location, but your program doesn’t have the same powers. It can’t know which player is at a specified position without directly accessing GameBoard
properties.
Add the following function at the end of GameBoard
:
func playerAtLocation(coordinates: (Int, Int)) -> PlayerColor { return board[coordinates.1][coordinates.0] } |
This function simply returns the color of the piece located at the given coordinates.
Often, when a function gets a value from the class, there’s also one to set a value. In this case, setting the PlayerColor
at a location can be very helpful.
If you need to set up the board, allow someone else to take over a piece, or need to remove a jumped piece, you need a method like this. Add the following function at the end of GameBoard
.
func setPlayer(player: PlayerColor, atLocation coordinates: (Int, Int)) { board[coordinates.1][coordinates.0] = player } |
This method replaces the color of the piece at a specific location on the board with a piece of the given color.
Declaring Subscripts
Subscripts and computed properties have similar declarations. For a little comparison, pretend you’re making a computed property, like this:
var computedProperty: Type { get { //return someValue } set { //setSomeValue() } } |
Here’s how that looks as a subscript:
subscript(parameters) -> ReturnType { get { //return someValue } set (newValue) { //setSomeValue() } } |
In the setter, there is a default parameter newValue
with a type that equals the subscript’s return value type. You use this parameter to change the underlying value within the class. You don’t have to declare the newValue
parameter.
Just like computed properties, subscripts can be read-only or read-write.
A read-only subscript doesn’t explicitly state get or set; the entire function body is a getter:
subscript(parameters) -> ReturnType { return someValue } |
A common approach to subscripting is to have a normal method that works the same as the subscript, allowing it to act as a shortcut to the method.
For example, consider that NSMutableArray
has objectAtIndex()
and replaceObjectAtIndex(withObject:)
. Using subscripts on an NSMutableArray
instance would call these two methods, depending on the use.
Similarly, custom indexed subscripts on your class in Objective-C serve as shortcuts for calls to objectAtIndexedSubscript:
and setObject:atIndexedSubscript:
, both of which you must implement appropriately for your class.
In Swift, Apple seems to have moved away from this paradigm, because using subscripts is the only way to access elements of arrays and dictionaries. However, this paradigm is still useful.
Creating a Subscript on GameBoard
Remember playerAtLocation(_:)
and setPlayer(_:atLocation:)
? A single subscript can replace both of these.
Add this empty subscript declaration into the bottom of GameBoard
:
subscript(coordinates: (Int,Int)) -> PlayerColor { get { } set { } } |
Ignore the errors for now, they’ll disappear when you combine playerAtLocation()
and setPlayer(atLocation:)
into a single read-write subscript.
Based on the pattern explained earlier, there is no need to copy the code from before into the subscript. Instead, the subscript calls these methods directly.
Add the following code to the subscript’s getter:
get { return playerAtLocation(coordinates) } |
At the bottom of the playground, add the following lines to see this subscript in action:
let player = board[(3,4)] println(player.description) |
Since there is a black piece at the location (3,4), the console prints Black.
Likewise, implement the setter using setPlayer(atLocation:)
to make your code look like this:
set { setPlayer(newValue, atLocation: coordinates) } |
Now go to the bottom of the playground and enter the following lines:
board[(3,4)] = .Red board.displayBoard() |
Voila! The black piece located at (3,4) becomes a red piece!
-r-r-r-r r-r-r-r- -r-r-r-r -------- ---r---- b---b-b- -b-b-b-b b-b-b-b- |
Adding a Second Subscript
One subscript down, one more to go! This time, you’re adding a subscript for movePieceFrom(_:to:)
. After this, instead of writing this old, verbose line:
board.movePieceFrom((2,5), to: (3,4)) |
You’ll be able to write the more succinct:
board[(4,5)] = (2,3) |
At the bottom of GameBoard
, create another read-write subscript like the first one, but with a return type of (Int,Int)
instead of PlayerColor
.
subscript (coordinates: (Int,Int)) -> (Int,Int) { get { } set { } } |
Add movePieceFrom(to:)
to the setter for this subscript.
set { movePieceFrom(coordinates, to: newValue) } |
Note the above code uses the default newValue
, but doesn’t declare it.
In contrast, the getter for this subscript is not so straightforward. In fact, you don’t want to return a value for this subscript; it’s effectively write-only.
Instead of returning a dummy value, you’ll employ the useful assert
function, which throws an error during development to alert the developer that something’s wrong, but doesn’t throw an error in production code.
Replace the getter with the following:
get { assert(false, "Using the getter of this subscript is not supported.") } |
Now, any attempt to access this subscript throws an exception.
Uh-oh, there seems to be a problem! Did you notice that the playground shows an error on the line?
let player = GameBoard[(3,4)]
.
Why is this happening? More importantly, how can you fix it?
Ambiguous Subscripts
The problem is a lack of information. GameBoard[(3,3)]
can return either (Int,Int)
or PlayerColor
. As a result, the compiler has no clue know which one to call.
In order to specify which one you really want to use, explicitly set the type of player
to a PlayerColor
. The compiler will then be able to infer the appropriate subscript method to use.
To do this, replace the line:
let player = board[(3,4)] |
With this:
let player: PlayerColor = board[(3,4)] |
This lets the compiler know which subscript you really want to call: the one that returns a PlayerColor
.
Gratuitous Gameplay
Right now, when you use the subscript to set the new position of a piece, the piece just goes wherever you say, even if it’s an illegal or invalid move. Talk about a power trip.
You’ll have a hard time finding anybody to play with you given your supreme impunity to the rules, so you need to add some simple move validations.
Before jumping to the code, think through the ways you can ensure a move is valid.
- Is the new coordinate in the game board? If either of the coordinates are outside the range of 0-7, the move is invalid.
- Is the moving piece either red or black? If the piece represents a blank space on the board, there’s no sense in moving it.
- Is there a red or black piece at the new location? In checkers, you cannot land on another piece.
- Is the piece moving in the right direction? red pieces move down the board, while black pieces move up the board.
- Is the new position off by one in both the X-coordinate and the Y-coordinate? Pieces move diagonally by one when they don’t jump over the opponent’s piece.
- Does the piece jump over an opponent?. If this is the case, then a blank space must replace the jumped piece on the board.
That of course doesn’t cover every variable, but you have the basics covered.
To be fully functional, you also need to check for multiple jumps, declare a king, allow the king to move backwards, and check for moves that are not jumps when a jump is possible (The official rules of checkers requires a player to make a jumping move if one is possible.)
You’d also need to check that the player moving is not moving out of turn!
Now that you know what to validate, you need to add these new rules to the program. Instead of making changes directly to the subscript, you’ll make the changes in movePieceFrom(_:to:)
.
Delete the code inside movePieceFrom(_:to:)
. One by one, you’ll add the rules from your list.
After detecting an invalid move, the code prints an error message. Within the function, add this new function that prints the error message:
func error(errorType:String) { println("Invalid Move: \(errorType)") } |
The first check is to make sure both locations are actually on the board, not in the hinterlands. To do this, check if the coordinates fall within the proper range of 0-7:
if !(0...7 ~= from.0 && 0...7 ~= from.1 && 0...7 ~= to.0 && 0...7 ~= to.1) { error("Range error") return } |
The ~=
operator (tilde and equals characters) tests to see if the range on the left contains the value on the right. Here, you make sure that each location’s coordinate is in the range of 0-7. If not, you call the error message and return
.
Next up is a simple check to ensure the color of the moved piece is a not a blank, and in this situation, you’ll use subscripting to find the player.
if playerAtLocation(from) == .None { error("No Piece to Move") return } |
Similar to the previous validation, you check if there is actually a piece (of either color) at the starting position.
Now, add this to the bottom of movePieceFrom(_:to:)
:
if playerAtLocation(to) != .None { error("Move onto occupied square") return } |
In this case, you do want the new location to be empty. Notice the !=
compared to the ==
in the previous code.
Onto the next validation step, which is making sure the piece is moving in the right direction. Remember that each color can only go one way — and that’s what this next block does:
let yDifference = to.1 - from.1 if (playerAtLocation(from) == .Red) ? yDifference != abs(yDifference) : yDifference == abs(yDifference) { error("Move in wrong direction") return } |
yDifference
finds the difference between the y-coordinates of to
and from
.
If the piece being moved is red, then it can only move down the board; when a piece moves down the board, it moves by a positive value. Since a positive value always equals its absolute value, you can use this basic math to determine if the piece is moving in the right direction.
On the other hand, black moves up the board and has a negative y-coordinate. Negative numbers don’t equal their absolute value so they’re disallowed from moving down the board — you check this at the end of the ternary operator.
Now for the next validation, and that is determining if the piece is moving diagonally by one. This is the only type of move you can make when you’re not moving a king and there isn’t a jump to make.
Diagonal movement requires that both the x- and y-coordinate change by an absolute value of 1. The following checks for this condition:
if abs(to.0 - from.0) != 1 || abs(to.1 - from.1) != 1 { error("Not a diagonal move") return } |
movePieceFrom(_:to:)
is almost functional at this point, but there is an issue to address.
Since the last segment throws an error whenever a piece moves more than one space, there is no way to check for legal jumps that move a piece by 2 spaces.
In order to change this, replace the code:
if abs(to.0 - from.0) != 1 || abs(to.1 - from.1) != 1 { error("Not a diagonal move") return } |
with:
if abs(to.0 - from.0) != 1 || abs(to.1 - from.1) != 1 { if abs(to.0 - from.0) != 2 || abs(to.1 - from.1) != 2 { error("Not a diagonal move") return } let coordsOfJumpedPiece = ((to.0 + from.0) / 2 as Int, (to.1 + from.1) / 2 as Int) let pieceToBeJumped: PlayerColor = self[coordsOfJumpedPiece] if contains([.None, playerAtLocation(from)], pieceToBeJumped) { error("Illegal jump") return } setPlayer(.None, atLocation: coordsOfJumpedPiece) } |
The first if-statement is the same as the original code, but inside there’s another similar if-statement that checks to see if the piece moves by 2 instead of by 1. Also, if the piece doesn’t move diagonally by 1 or 2, the code prints an error message to the console.
In the instance that the piece moves two spaces in either direction, there needs to be a piece in the middle to jump over, otherwise the move is illegal. The location of this piece must be the midpoint between from
and to
, and you calculate that location by averaging the x and y values of both coordinates.
Also, the jumped piece needs to be the opposite color as the piece that is doing the jumping. Once the code confirms that, the jumped piece is set to .None
.
Nice job, although you’ll no longer enjoy supreme command over the board, you now have checks in place that allow for basic play and the piece moves to its new location:
let pieceToMove = board[from.1][from.0] board[from.1][from.0] = empty() board[to.1][to.0] = pieceToMove |
Finally, test out this new functionality by entering these two lines at the bottom of the playground:
board[(4,5)] = (2,3) board.displayBoard() |
This changes the board from
-r-r-r-r r-r-r-r- -r-r-r-r -------- ---r---- b---b-b- -b-b-b-b b-b-b-b- |
to
-r-r-r-r r-r-r-r- -r-r-r-r --b----- -------- b-----b- -b-b-b-b b-b-b-b- |
The black piece now jumps over the red piece, as you’d expect in a normal game of checkers.
Now test the error checking by trying other moves. Remember that each legal move you make changes the board!
Enter the moves below, one by one, in the order indicated to see the effect:
board[(1,6)] = (2,5) //legal move board.displayBoard() board[(1,6)] = (1,5) //No piece to move since board.displayBoard() //you just moved the piece at (1,6) board[(7,2)] = (6,1) //(6,1) is already occupied board.displayBoard() board[(7,2)] = (5,4) //Illegal jump - no piece to jump board.displayBoard() board[(7,2)] = (8,1) //Range Error board.displayBoard() board[(0,7)] = (1,6) //legal move board.displayBoard() |
Where To Go From Here?
If you’d like to see the project in its fully functional form, feel free to download the completed project from this Swift tutorial.
Now that you have added subscripts to your tool kit, look for opportunities in your own code to use them. While they don’t add extra functionality, they sure make your code more readable and more intuitive if used properly.
That said, you don’t always want to revert to subscripts. One or two subscripts on a class can be helpful, but if the subscript feels unnatural or forced, or if you have more than a few on a single class, then you should start asking yourself if you really need it.
Of course, this is just a bare bones start for a checkers game. You can still improve the game, for one, there’s still several basic validations you could add. Then you could implement a “King Me!” mechanism, multiple jumps, or track of which player’s turn it is.
If you want to learn more about Swift development, check out our book Swift by Tutorials.
If you have an questions, comments or other idea for how to use subscripts, please leave your comments below!
Custom Subscripting in Swift Tutorial is a post from: Ray Wenderlich
The post Custom Subscripting in Swift Tutorial appeared first on Ray Wenderlich.