SwiftUI | Data flow | Tic-Tac-Toe Game

Manisha Roy
Globant
Published in
5 min readAug 1, 2022

This is part 6 of the Tic-Tac-Toe Game series. In this article, we will learn how data flows in SwiftUI and add some rules to our game, Tic-tac-toe. SwiftUI usage property wrapper for passing data and managing their states throughout the app.

The first rule checks whether the game has ended or not. The game will end if all the grids have been selected or if the winner is announced. We will first focus on grid selection, for that we have one flag noOfStrikes inside GameSettings which tracks the number of taps by both the players.

Please refer SwiftUI Drawing article to access the latest code.

Jump back to onTapGesture of Rectangle inside MatrixBlock view. Here we will increment noOfStrikes just after updating gridDetail:

...
self.gridDetail.player = self.settings.currentPlayer
settings.noOfStrikes += 1
...

We have one more flag isGameEnded inside GameSettings which publishes its update to observing views. Add one function setGameEnded inside GameSettings which will update this flag.

func setGameEnded() {
if noOfStrikes == 9 {
isGameEnded = true
}
}

We have 9 grids on our checkerboard that’s why 9 strikes will end the game. We will use isGameEnded publisher’s state to show EndGameView below the checkerboard.

Create a new SwiftUI file naming it EndGameView where just replace the content of Text view with Game ended. The code will look like this:

struct EndGameView: View {
var body: some View {
Text(“Game ended”)
.multilineTextAlignment(.center)
.font(.largeTitle)
}
}

The game has ended but the winner is yet not decided. The Tic-tac-toe game has these rules for deciding the winner:

  • The same player played in a single line.
  • The same player played in a single column.
  • The same player played diagonally.

In the above screenshot, we can see that player 2 is a winner as s/he has marked X diagonally.

To decide the winner, we need to track each grid on which player has played. MatrixBlock has GridDetail which stores enough details of the grid for deciding the winner. but the problem is that this individual GridDetail cannot be tracked for implementing the above rules as MatrixGrids is drawing straightforward 3*3 MatrixBlock using ForEach. Let's first club the GridDetail instances in one place so that their data can be accessed easily and will always be in sync.

Create one new struct RowGridDetails which will store a collection of GridDetail for a single row. We will save this RowGridDetails instance inside GameSettings as this 3*3 checkerboard belongs to the game setting.

struct RowGridDetails {
var grids: [GridDetail]
}
class GameSettings: ObservableObject {
var gridDetails: [RowGridDetails] = [
RowGridDetails(grids: [GridDetail(), GridDetail(), GridDetail()]),
RowGridDetails(grids: [GridDetail(), GridDetail(), GridDetail()]), RowGridDetails(grids: [GridDetail(), GridDetail(), GridDetail()])]
...

This will allow us to have a checkerboard n*n or n*m size.

Now iterate through this gridDetails for creating MatrixGrids.

After updating the code you will get this error:

Referencing initializer ‘init(_:content:)’ on ‘ForEach’ requires that ‘RowGridDetails’ conform to ‘Identifiable’

To fix this error we need to confirm the Identifiable protocol to RowGridDetails struct. On confirming it you will be suggested to implement

var id: ObjectIdentifier

id creates an instance that uniquely identifies the given class instance. This will be used by SwiftUI to identify which view to update/delete etc. id can be Int as well. If id is declared without the initialization then it will lead to passing an extra argument while initializing RowGridDetails instances. There are two ways to avoid this extra argument. First, initialize this at the time of declaration with some random integer value which will lead to having the same value for all the RowGridDetails hence uniqueness gets compromised. The other way suggests initializing it with UUID which fixes the first error and also uniqueness is maintained.

var id = UUID()

To fix the second error, we need to modify GridDetail but there we will not be asked to add id since its class type and class have unique ids attached to them.


class GridDetail: Identifiable {
var isSelected = false
var player = 0
}

After running the code, we will see the same results which we had till showing “Game ended”. Now let us start adding rules for finding the winner.

private func isWinner(player: Int) -> Bool {
for row in gridDetails {
if row.grids.filter({$0.player == player}).count == 3 {
return true
}
}...

A player number is passed as an argument for which we want to know the winning status. First, we are iterating through rows of checkerboard and filter player details for each grid. Since we have a 3*3 grid, return true if all three grids are selected by the same player.

...
for index in 0..<gridDetails.count {
if gridDetails.filter({$0.grids[index].player == player}).count == 3 {
return true
}
}

This is iterating through columns of the checkerboard, again returning true if the count is 3.

if gridDetails[0].grids[0].player == player,
gridDetails[1].grids[1].player == player,
gridDetails[2].grids[2].player == player {
return true
}
if gridDetails[0].grids[2].player == player,
gridDetails[1].grids[1].player == player,
gridDetails[2].grids[0].player == player {
return true
}
return false

It will check diagonally from top-left to bottom-right and if a winner is not found then diagonally from top-right to bottom-left. And at last, it will return false if none of the 4 conditions is satisfied. This is for one player, so we need to call it twice for both the players like this to decide a winner:

func setWinner() {
if isWinner(player: PlayerInfo.player1.rawValue) {
winner = PlayerInfo.player1.rawValue
}
if isWinner(player: PlayerInfo.player2.rawValue) {
winner = PlayerInfo.player2.rawValue
}
}

We will call this method inside the onTapGesture of MatrixBlock:

settings.noOfStrikes += 1
settings.setWinner()
settings.setGameEnded()

Our winner is decided but the game is not ended yet. To end the game once a winner is decided, just add a check for winner status inside the setGameEnded function:

if winner != 0 || noOfStrikes == 9 {

Won’t be a good idea to show who won the game? Let’s achieve it by modifying EndGameView.

struct EndGameView: View {
var winner: Int
var body: some View {
VStack {
Text(“Game ended”)
let message = winner == 0 ? “It’s a tie!!” : “Player \(winner) isa winner”
Text(“\(message)”)
.foregroundColor(PlayerInfo.init(rawValue: winner)?.getPlayerColor())
.bold()
}.multilineTextAlignment(.center)
.font(.largeTitle)
}
}

Just run it and play the game. You will be able to see the winner details or the game tie info. Restrict the players to continue playing once a winner is decided, for that add the isGameEnded flag before performing any operation.

Cheers to us!!! We have our own tic-tac-toe game that too fully functional.

Do check out my other articles in this series:

Text view styling

Button

Navigation

Shapes

Drawing

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!!