-
Book Overview & Buying
-
Table Of Contents
Swift Cookbook - Third Edition
By :
By using collection types, we have seen that their elements are accessed through subscripts. However, it’s not just collection types that can have subscripts; your own custom types can provide subscript functionality too.
In this recipe, we will create a simple game of tic-tac-toe, also known as noughts and crosses. To do this, we need a three-by-three grid of positions, with each position being filled by either a nought from player 1, a cross from player 2, or nothing. We can store these positions in an array of arrays.
The initial game setup code uses the concepts we’ve already covered in this book, so we won’t go into its implementation. Enter the following code into a new playground so that we can see how subscripts improve its usage:
enum GridPosition: String {
case player1 = "o"
case player2 = "x"
case empty = " "
}
struct TicTacToe {
var gridStorage: [[GridPosition]] = []
init() {
gridStorage.append(Array(repeating: .empty,
count: 3))
gridStorage.append(Array(repeating: .empty,
count: 3))
gridStorage.append(Array(repeating: .empty,
count: 3))
}
func gameStateString() -> String {
var stateString = "\n"
stateString += printableString(forRow: gridStorage[0])
stateString += "\n"
stateString += printableString(forRow: gridStorage[1])
stateString += "\n"
stateString += printableString(forRow: gridStorage[2])
stateString += "\n"
return stateString
}
func printableString(forRow row: [GridPosition]) -> String {
var rowString = "| \(row[0].rawValue) "
rowString += "| \(row[1].rawValue) "
rowString += "| \(row[2].rawValue) |\n"
return rowString
}
}
Let’s run through how we can use the tic-tac-toe game defined previously, as well as how we can improve how it is used, using a subscript. We will also examine how this works:
TicTacToe grid:var game = TicTacToe()
GridPosition value that’s been assigned to the relevant place in the array of arrays. This is used to store the grid positions. Player 1 will place a nought in the middle position of the grid, which would be row position 1 and column position 1 (since it’s a zero-based array):// Move 1 game.gridStorage[1][1] = .player1 print(game.gameStateString()) /*
|
| |
| |
| |
| |
|
| |
| |
o | |
| |
|
| |
| |
| |
| |
|
*/ |
Figure 2.9 – The grid positions
3. Then, player 2 places their cross in the top-right position, which is row position 0 and column position 2:
// Move 2 game.gridStorage[0][2] = .player2 print(game.gameStateString()) /* ||| x | || o || |||| */
We can make moves in our game. We can do this by adding information directly to the gridStorage array, which isn’t ideal. The player shouldn’t need to know how the moves are stored, and we should be able to change how we store the game information without having to change how the moves are made. To solve this, let’s create a subscript of our game struct so that making a move in the game is just like assigning a value to an array.
4. Add the following subscript method to the TicTacToe struct:
struct TicTacToe {
var gridStorage: [[GridPosition]] = []
//...
subscript(row: Int, column: Int) -> GridPosition {
get {
return gridStorage[row][column]
}
set(newValue) {
gridStorage[row][column] = newValue
}
}
//...
}
5. So, now, we can change how each player makes their move and finish the game:
// Move 1 game[1, 1] = .player1 print(game.gameStateString()) /*
|
| |
| |
| |
| |
|
| |
| |
o | |
| |
|
| |
| |
| |
| |
|
*/ |
// Move 2 game[0, 2] = .player2 print(game.gameStateString()) /* ||| x | || o || |||| */ // Move 3 game[0, 0] = .player1 print(game.gameStateString()) /* | o || x | || o || |||| */ // Move 4 game[1, 2] = .player2 print(game.gameStateString()) /* | o || x | || o | x | |||| */ // Move 5 game[2, 2] = .player1 print(game.gameStateString()) /* | o || x | || o | x | ||| o | */
6. Just like when using an array, we can use a subscript to access the value, as well as assign a value to it:
let topLeft = game[0, 0] let middle = game[1, 1] let bottomRight = game[2, 2] let p1HasWon = (topLeft == .player1) && (middle == .player1) && (bottomRight == .player1)
Subscript functionality can be defined within a class, struct, or enum, or declared within a protocol as a requirement. To do this, we can define subscript (which is a reserved keyword that activates the required functionality) with input parameters and an output type:
subscript(row: Int, column: Int) -> GridPosition
This subscript definition works like a computed property, where get can be defined to allow you to access values through subscript, and set can be defined to assign values using subscript:
subscript(row: Int, column: Int) -> GridPosition {
get {
return gridStorage[row][column]
}
set(newValue) {
gridStorage[row][column] = newValue
}
}
Any number of input parameters can be defined, and these should be provided as comma-separated values in the subscript:
game[1, 2] = .player2 // Assigning a value let topLeft = game[0, 0] // Accessing a value
Just like parameters defined in a function, subscript parameters can have additional labels. If defined, these become required at the call site, so the subscript we added can alternatively be defined as follows:
subscript(atRow row: Int, atColumn column: Int) -> GridPosition
In this case, when using subscript, we would also provide the labels in it:
game[atRow: 1, atColumn: 2] = .player2 // Assigning a value let topLeft = game[atRow: 0, atColumn: 0] // Accessing a value
Further information about subscripts can be found in Apple’s documentation on the Swift language at http://swiftbook.link/docs/subscripts.