This blog post is the third and final update about my Tic-tac-toe game, written in Swift. The source code is available on GitHub:
The app now has a user interface, which allows you to play against the computer or against another person on the same iPhone.
In my article about the Tic-tac-toe gameplay and data model I presented a diagram of domain components, such as the Game and GameBoard classes. The article about artificial intelligence reviewed a perfect-opponent strategy implementation. Now that the app has a user interface which ties these components together, we can see how they fit into the bigger picture.
The containment relationship between Game and GameBoard (i.e. a Game has a GameBoard) is mirrored by GameViewController and GameBoardView. When GameViewController begins a new game of Tic-tac-toe, it creates a GameBoard used as the data source for both a Game and GameBoardView.
The state of a game is visually depicted by GameBoardView. That UIView subclass relies on GameBoardRenderer and GameBoardLayout to perform drawing work.
As the code comments above indicate, creation of the layout and renderer objects is delayed until drawRect(_:) is called to ensure that the view’s graphics context and size are valid. Each object has two properties; one with an underscore prefix to store the object and another, without a prefix, that creates the object when necessary. This is similar to having a custom property accessor that uses an instance variable, in Objective-C.
The rendering work is performed by GameBoardRenderer:
All of the rendering routines are defined in a private class extension:
This code might seem odd if you have experience with CoreGraphics, which is an API of C functions and structures. Thanks to Swift’s all-encompassing support for adding methods to types, I was able to create this wrapper API to hide the ugly details of using CGContext.
Rendering a user interface does not, and should not, involve determining the size and location of each thing in the user interface. This separate responsibility is often referred to as layout calculation. I separated these two responsibilities by having the renderer rely on GameBoardLayout for the size and location of every user interface element. The rendering methods seen above use these properties of a layout object:
These properties are all lazy in order to avoid calculating their values more than once. This optimization is appropriate because a layout object is used multiple times throughout a game.
There is a lot more going on in this app, so if you’re interested be sure to clone the repository and dig in!