Skip to content

Programming with Structures

In this chapter, you’ll revisit the implementation of Tic-Tac-Toe. You’ll identify the types in this program and introduce them into the code. This will add structure and reduce the overall complexity.

You’ll be guided through the changes you need to make, but you won’t see a full code listing for each change — some changes are left as exercises. The solutions bundle includes a solution for this chapter so that you can verify your code at the end.

To begin, open the solution project for Tic-Tac-Toe. Take a moment to refamiliarize yourself with the code. When you’re ready, open board.swift to begin making changes.

Type Board

The contents of board.swift implement the game board:

  • Variables topLeft to bottomRight hold the state of the board.
  • Functions printBoard, boardIsFull, and resetBoard read or modify this state.

This combination of variables that hold state and functions that operate on them suggests a type: a Board. You can think of the contents of board.swift as the properties and methods of this type.

To create type Board, add the following structure to board.swift:

swift
struct Board {

}

Once this type is fully fleshed out with properties and methods, it’ll replace the other code in board.swift.

Properties

Your new type needs properties to store the state of a board. There are several ways you can do this. An obvious solution is to use the current nine variables:

swift
struct Board {

  var topLeft: String
  var top: String
  var topRight: String
  var left: String
  var center: String
  var right: String
  var bottomLeft: String
  var bottom: String
  var bottomRight: String
}

However, as you saw in Arrays, you can also use an array:

swift
struct Board {
  
  var positions: [String]
}

This is far better than using nine separate properties. There’s still room for improvement, though.

Every position holds either a player symbol ("O" or "X") or, if the position is empty, a number string ("1" to "9"). However, these numbers aren’t part of the game; they’re how you choose to display empty positions.

Separating how data is stored from how it’s displayed is an important design principle in software development. Therefore, it’s better to rely on optionals and store empty positions as nil:

swift
struct Board {
  
  var positions: [String?]
}

The positions property is now focused on storing the board. The printBoard function will decide how to display it.

Next, add an initializer to create an empty board:

swift
struct Board {

  var positions: [String?]

  init() {
    positions = Array(repeating: nil, count: 9)
  }
}

Alternatively, you can set a default value for positions:

swift
struct Board {

  var positions: [String?] = Array(repeating: nil, count: 9)
}

However, by declaring a custom initializer, you disable the memberwise initializer and make it clear that the board always starts with nine empty positions.

Your initializer takes no parameters, so you can create a board as follows:

swift
var board = Board()

However, don’t do this yet. board.swift should only declare type Board. You’ll create instances where they’re needed.

Methods

Next, consider the functions printBoard, boardIsFull, and resetBoard, and how you can incorporate them into type Board.

printBoard is fairly straightforward. You can make it a method, like so:

swift
struct Board {
  // ...

  func print() {
    // ...
  }
}

This method is named print, not printBoard, in accordance with the API Design Guidelines mentioned in Functions. One of these guidelines is that names should not contain words (such as Board) that needlessly repeat type information.

The implementation of print is similar to the printBoard function it replaces, but you need to make a few changes:

  • Because print has the same name as the print function from the Standard Library, you must refer to the latter as Swift.print in this method, otherwise print will call itself.
  • Replace references to topLeft, left, and so on, with references to the corresponding elements of positions.
  • Use the nil-coalescing operator (??) to handle empty positions, for example: positions[0] ?? "1".

Next, consider boardIsFull. This function can become a computed property:

swift
struct Board {
  // ...

  var isFull: Bool {
    for element in positions {
      if element == nil {
        return false
      }
    }
    return true
  }
}

This implementation uses a for-in loop, which is efficient but not very clear. The loop searches for an element that’s nil, which indicates that the board is not full. This inverted logic is what makes the code difficult to understand at a glance.

Fortunately, type Array has a method that can improve this code:

swift
var isFull: Bool {
  positions.allSatisfy { $0 != nil }
}

This implementation clearly conveys that a board is full when all of its positions are no longer nil.

Finally, consider resetBoard. This function can become a reset method:

swift
struct Board {
  // ...

  mutating func reset() {
    positions = Array(repeating: nil, count: 9)
  }
}

This method modifies the state of the board, so it must be marked as mutating.

Using type Board

Now that type Board is complete, you can remove the other code from board.swift as it’s no longer needed.

Try to build the project. You’ll see lots of errors, but don’t let these scare you. The compiler is telling you that some of your code uses the now-deleted global variables and functions. You need to fix that.

Start by updating input.swift. The readPosition function checks the user’s input against the board to verify that the user has selected an empty position. The board is no longer stored in global variables, so you need to pass it as a parameter:

swift
func readPosition(on board: Board) -> Int {
  // ...
}

Additionally, make the following changes to readPosition:

  • Remove the switch statement. You no longer need it now that the positions are stored in an array.
  • Remove the forced unwrap from Int(input)! and use optional binding to handle input that isn’t a number.
  • Return an index between 0 and 8, not between 1 and 9. This avoids off-by-one errors in code that uses this index to read from the positions array.

After you make these changes, move on to game.swift. This is where you create and store an instance of Board:

swift
var board = Board()

It makes sense to store this here because the board is a part of the game.

Next, make the following changes:

  • Replace all references to the now-removed global variables and functions with references to the properties and methods of board.
  • Reimplement takeTurn. You can simplify this function quite a bit now that you’re storing the board in an array.
  • Reimplement currentPlayerHasWon. In Tuples, you saw an implementation that uses tuples. Can you build a similar one that uses closures?
  • Remove the selectedPosition variable. Your new implementations of takeTurn and currentPlayerHasWon should no longer need it.

Your code should now be back to a working state. Compile and run it to verify. If everything works, open game.swift to continue making changes.

Type Game

game.swift contains the following variables and functions:

  • currentPlayer, gameIsOver, and board track the state of the game.
  • initializeGame initializes this state.
  • playGame plays a game of Tic-Tac-Toe.
  • takeTurn, switchPlayers, and currentPlayerHasWon are helper functions used by playGame.

These variables and functions are related and suggest a type: a Game. You’ll create this type now.

Add an empty Game structure to game.swift:

swift
struct Game {

}

Next, add properties board, currentPlayer, and isOver:

swift
struct Game {

  var board: Board
  var currentPlayer: String
  var isOver: Bool
}

Initialize these properties using the code from initializeGame:

swift
struct Game {
  // ...

  init() {
    board = Board()
    currentPlayer = Bool.random() ? "O" : "X"
    isOver = false
  }
}

Next, add takeTurn and currentPlayerHasWon as methods. takeTurn changes the state of the game, so it must be a mutating method.

Finally, add a mutating play method using the code from playGame. Make the following changes to this code:

  • Fix any references to gameIsOver, which is now named isOver.
  • Replace the call to switchPlayers with a statement that uses the ternary operator (?:). There’s no need to create a method for a task you only perform once and that only takes a single line of code.
  • Remove the call to initializeGame.

Type Game is now complete. You can remove the other code from game.swift.

Before you can run your code, you need to update main.swift. Inside the main loop, create an instance of Game and invoke its play method to start a game:

swift
var game = Game()
game.play()

Now that you have a Game type, you can create a new instance for every game you want to play. You no longer have to reset the game so you can repeatedly call play.

With these changes, you can compile and run your code again.

Type Input

So far, you’ve replaced the global variables and functions in board.swift and game.swift with properties and methods on types. But what about the global functions in input.swift? Should you move them into a type too?

Functions readPosition and readBool are different from the functions in board.swift and game.swift. They stand on their own and don’t depend on any state, so there’s nothing wrong with keeping them as global functions. However, you can avoid name conflicts with other global functions by moving them into a type as static methods.

Create a type Input, and move readPosition and readBool into this new type:

swift
struct Input {

  static func readPosition(on board: Board) -> Int {
    // ...
  }

  static func readBool(question: String) -> Bool {
    // ...
  }
}

Next, update any references to these methods. Because they’re static methods, you call them as Input.readPosition and Input.readBool. You never need to create any instances of type Input. It only serves as a home for the static input methods.

Finally, rename your files to Board.swift, Game.swift, and Input.swift. It’s common practice to declare a single type per file and to name the file after this type. This makes navigating your project easier.

Encapsulation

Grouping related variables and functions into types not only adds structure to your code, but it also creates an opportunity to add encapsulation.

Encapsulation is an important aspect of software design. When designing types, you want to design them in such a way that they expose the least amount of information. What matters only is the functionality a type provides, not how it implements this functionality. Types shouldn’t know or care about another type’s internals.

A type can distinguish between the members it provides to other types and members that are part of its implementation. Collectively, the members a type provides to other types are known as its application programming interface (API). Swift’s API Design Guidelines promote clear and consistent naming of API members.

The private keyword marks members as private and hides them from other types. You’ll use this keyword to improve the encapsulation of your types.

Game

First, consider type Game. Only two of its members are used outside of the type: main.swift uses the initializer and the play method. You can make all other members of Game private to encapsulate them.

Encapsulation doesn’t limit your programming freedom. On the contrary, because you only expose init and play, you’re free to change the rest of the type as you see fit. As long as you don’t change the name or type of an exposed member, your changes will not affect other types.

Board

Next up is type Board. Type Game uses all of its members, so there isn’t much you can encapsulate. However, the positions array deserves special attention.

Currently, positions is fully exposed. Although Game does need to read and modify individual positions, it should never replace the array as a whole. To protect against this, mark positions as read-only using a variation of the private keyword:

swift
private(set) var positions: [String?]

This exposes positions as a read-only property, with write access (set) being private to type Board.

Next, add a setPosition method so that Game can modify a single position on the board:

swift
mutating func setPosition(_ position: Int, to player: String) {
  positions[position] = player
}

This new design better communicates how you intend to use type Board:

  1. Create an empty board with the initializer.
  2. Fill the positions one at a time with setPosition.
  3. Call reset to start over.

This change to Board triggers a compilation error. Resolve the error by changing the takeTurn method in Game to use setPosition.

Input

Finally, there’s type Input, which serves as a home for the static input methods. You never need to instantiate this type, yet it exposes an initializer, which is confusing:

swift
let input = Input()  // ?

Remove this confusion by declaring a private initializer:

swift
private init() { }

This disables the automatic initializers and makes it impossible to create an instance of Input outside of the type itself, which better communicates the purpose of the type.

Up next

This chapter showed you how to restructure an existing application using structures. In the next chapter, you’ll learn about a different category of types, enumerations, and improve the implementation of Tic-Tac-Toe even more.