SwiftUI | Drawing | Tic-Tac-Toe Game

Manisha Roy
Globant
Published in
5 min readJul 28, 2022

This is part 5 of the Tic-Tac-Toe Game series. In this tutorial, we will learn SwiftUI Drawing and create a checkerboard for our game.

SwiftUI has a Shape protocol which we need to confirm while drawing custom paths. Shape protocol has a path(in:) method which accepts a CGRect and returns a path. Using Shape protocol we can draw SwiftUI inbuilt shapes like rectangles, circles etc. Fortunately, We can reuse our previously drawn paths using CGPath or UIBezierPath in SwiftUI as well.

We will draw the X mark using a custom path. For that, we will create a new SwiftUI file and name it to XMarkView. Please note that View accepts Shape type that’s why we will make changes like this:

Here we need to implement a mandatory method, path(in rect: CGRect) -> Path. Create and initialize one path variable and return it from this method to solve this build time error.

To draw X we need two lines/paths diagonally. Let’s draw the first line as:

path.move(to: CGPoint(x: rect.minX, y: rect.minY))
path.addLine(to: CGPoint(x: rect.width, y: rect.maxY))

Move to the top-left corner then a line will be drawn from top-left to bottom-right

path.move(to: CGPoint(x: rect.width, y: rect.minY))
path.addLine(to: CGPoint(x: rect.minX, y: rect.height))

We need to move to the new point, top-right as our line is not continuous. After that, a new line will be drawn from top-right to bottom-left.

Nothing will be visible on preview as we need to give stroke or pencil color along with bound details otherwise it will take the whole screen/parent views dimension. The final look will be like this:

We will add closeSubpath() once the drawing is completed and just before returning the newly computed shape so that graphics will start drawing new paths without calling move(to:).

For O, use SwiftUI in-built circle shape.

Our X and O marks are ready. Next, we need to design a checkerboard using Path.

Create a new SwiftUI file by naming it as MatrixBorder of type Shape just like we did for XMarkView. Below example illustrate how the checkerboard is drawn.

Once a random player is selected to play first, we will show this checkerboard. Go back to GameScreen and create one state variable shoudAddCheckerboard which will be initially false and will be set to true once the baton animation is completed and a player is selected:

DispatchQueue.main.asyncAfter(deadline: .now() + 12) {
self.shoudAddCheckerboard = true
}

We will add MatrixBorder conditionally inside the GameScreen body.

if shoudAddCheckerboard {
MatrixBorder()
.stroke(Color.red, lineWidth: 3)
.frame(width: 150, height: 150)
}

After some more changes, we will be able to see this on the game screen.

Our checkerboard’s UI is ready but it’s not performing any operation on its tap. Because the checkerboard is just a shape containing 4 paths/lines. To make it intractable with player’s action, we need to add one grid of 3*3 blocks where each block will draw X or O as per player’s turn.

Create one struct by naming MatrixBlock. This view will interact with players.

struct MatrixBlock: View {
var body: some View {
Rectangle()
.frame(width: 40.0, height: 40.0)
.foregroundColor(.white)
}
}

This is the grid where the player will tap and X or O will get drawn as per player’s turn. For storing grid details like who has selected that grid and whether that grid is already selected or not, we need to have one object model:

class GridDetail: ObservableObject {
@Published var isSelected = false
var player = 0
}

Add onTapGesture on Rectangle for updating GridDetail on player’s tap.

if self.gridDetail.isSelected == false {
self.gridDetail.isSelected = true
self.gridDetail.player = self.settings.currentPlayer
}

First, check whether that particular grid is already selected or not. If not, then make it selected and set which player has played on it. To know the current player detail we will use environmentObject GameSetting.

settings is an environmentObject hence we do not need to initialize it again for accessing the data.

Grid detail is updated but it’s not reflected in the settings. For that first update current player details:

if self.gridDetail.player == 2 {
self.settings.currentPlayer = 1
} else {
self.settings.currentPlayer = 2
}

Once data is updated, the circle will be drawn if selected by player 1 else X will be drawn. For that close onTapGesture method and add the below code:

if self.gridDetail.isSelected == true {
if self.gridDetail.player == 1 {
Circle()
.stroke(PlayerInfo.player1.getPlayerColor(), lineWidth: 5)
.frame(width: 20, height: 20, alignment: .center)
} else {
XMarkView()
.stroke(PlayerInfo.player2.getPlayerColor(), lineWidth: 5)
.frame(width: 20, height: 20, alignment: .center)
}
}

Till now we have designed our individual grid but we need 3*3 grids so let’s arrange it inside MatrixGrids struct like this:

struct MatrixGrids: View {
var body: some View {
VStack {
ForEach(0..<3) { _ in
HStack {
ForEach(0..<3) { _ in
MatrixBlock()
}
}
}
}
}
}

The inner for loop will arrange 3 grids horizontally then the outer for loop will draw 3 vertical grids containing them.

struct Checkerboard: View {
var body: some View {
MatrixGrids()
.foregroundColor(.red)
}
}

Just add these codes and call Checkerboard instead of MatrixBorder.

...
if shoudAddCheckerboard {
Checkerboard()
...

Here we have grids in red and checkerboard borders in white but we want it another way. To achieve it, use our MatrixBorder and add it as an overlay on MatrixGrids.

Isn’t it amazing that we have drawn our own functional tic-tac-toe game but wait we still need to add some rules to end game and choose a winner.

Do check out my other articles in this series:

Text view styling

Button

Navigation

Shapes

Data Flow

List

Animation

If you liked this article then please appreciate it with claps and comments. This will encourage me to write more!!!!

--

--

Manisha Roy
Globant
Writer for

An enthusiastic iOS Developer. Keep learning!!