Book Image

Swift Cookbook. - Second Edition

By : Keith Moon, Chris Barker
Book Image

Swift Cookbook. - Second Edition

By: Keith Moon, Chris Barker

Overview of this book

Swift is an exciting, multi-platform, general-purpose programming language, and with this book, you'll explore the features of its latest version, Swift 5.3. The book begins with an introduction to the basic building blocks of Swift 5.3, its syntax, and the functionalities of Swift constructs. You’ll then discover how Swift Playgrounds provide an ideal platform to write, execute, and debug your Swift code. As you advance through the chapters, the book will show you how to bundle variables into tuples or sets, order your data with an array, store key-value pairs with dictionaries, and use property observers. You’ll also get to grips with the decision-making and control structures in Swift, examine advanced features such as generics and operators, and explore functionalities outside of the standard library. Once you’ve learned how to build iOS applications using UIKit, you'll find out how to use Swift for server-side programming, run Swift on Linux, and investigate Vapor. Finally, you'll discover some of the newest features of Swift 5.3 using SwiftUI and Combine to build adaptive and reactive applications, and find out how to use Swift to build and integrate machine learning models along with Apple’s Vision Framework. By the end of this Swift book, you'll have discovered solutions to boost your productivity while developing code using Swift 5.3.
Table of Contents (14 chapters)
12
About Packt

Subscripts for custom types

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.

Getting ready

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 can 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
}
}

How to do it...

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:

  1. Let's create an instance of our TicTacToe grid:
var game = TicTacToe()
  1. For a player to make a move, we need to change the 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, column position 1 (since it's a zero-based array):
// Move 1 
game.gridStorage[1][1] = .player1
print(game.gameStateString())
/*
-------------
| | | |
-------------
| | o | |
-------------
| | | |
-------------
*/
  1. Next, Player 2 places their cross in the top-right position, which is row position 0, 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.

  1. 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
}
}
//...
}
  1. 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 |
-------------
*/
  1. 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)

How it works...

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

There's more...

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 the subscript, we would also provide the labels in the subscript:

game[atRow: 1, atColumn: 2] = .player2 // Assigning a value 
let topLeft = game[atRow: 0, atColumn: 0] // Accessing a value

See also

Further information about subscripts can be found in Apple's documentation on the Swift language at http://swiftbook.link/docs/subscripts.