SwiftUI | List | Tic-Tac-Toe Game

Manisha Roy
Globant
Published in
4 min readAug 5, 2022

This is part 7 of the Tic-Tac-Toe Game series. Till now we created a one-round tic-tac-toe game for deciding the winner. Let’s make it 3 rounds game and the winner will be the one who has won a maximum number of rounds. Also, we will explore the List view for designing the score table to show who won which round.

Create a new ObservableObject class and name it RoundWinnerInfo. We need only two properties to be tracked, the round and that round’s winner. Make it Identifiable as well to iterate through instances of this class for designing List rows.

class RoundWinnerInfo: Identifiable, ObservableObject {
var id = UUID()
var round: Int
@Published var winnerPlayer: Int = -1
init(round: Int) {
self.round = round
}
}
//Update GameSettings@State var rounds: [RoundWinnerInfo] = []init() {
rounds = [RoundWinnerInfo(round: 1), RoundWinnerInfo(round: 2), RoundWinnerInfo(round: 3)]
setupGridDetails()
}

We will use rounds for designing our score table. Create a new SwiftUI file and name it WinnerListView. Create one new struct named WinnerListRow which will be rows of the List/Table view.

@Binding var round: RoundWinnerInfo

WinnerListRow will have one instance of RoundWinnerInfo which will be bound with the all rounds array of @State property wrapper type. Each row will have three columns: round number, player 1 and player 2 details.

var body: some View {
HStack {
Text(“\(round.round)”)
.bold()
.modifier(WinnerListRowModifier())
if round.winnerPlayer != -1 {
if round.winnerPlayer == 1 {
Image(systemName: “checkmark”)
.modifier(WinnerListRowModifier())
} else {
Text(“ — “).modifier(WinnerListRowModifier())
}
if round.winnerPlayer == 2 {
Image(systemName: “checkmark”)
.modifier(WinnerListRowModifier())
} else {
Text(“ — “).modifier(WinnerListRowModifier())
}
}
}.multilineTextAlignment(.center)
}

If a round is played then we are showing a checkmark to the winner player and — is shown to the other player. A structure of type ViewModifier can be created and modifiers can be clubbed there to use it with multiple views, WinnerListRowModifier is an example of such a case.

struct WinnerListRowModifier: ViewModifier {
func body(content: Content) -> some View {
content
.frame(width: 80, height: 50, alignment: .center)
}
}

Rows are ready but it will be good to have headers as well to complete the look of the score table.

struct WinnerListHeader: View {
var body: some View {
HStack {
Text(“Round”).modifier(WinnerListRowModifier())
Text(“Player 1”).modifier(WinnerListRowModifier())
Text(“Player 2”).modifier(WinnerListRowModifier())
Spacer()
}.multilineTextAlignment(.center)
}
}

We can see that there are some glitches in the UI. We don’t need this grey color produced by List view. At this moment SwiftUI doesn’t have an inbuilt modifier for changing/removing this color but we can use Swift code for handling the table appearance. Add this modifier on List view:

...
.onAppear{
UITableView.appearance().backgroundColor = .clear
}.padding(EdgeInsets(top: -30, leading: 0, bottom: 0, trailing: 0))
...

Hold on!! Everything will be white on the screen. Let’s change the row color to light grey color to distinguish it from the parent view.

WinnerListRow(round: round).listRowBackground(settings.disabledColor)
Score table with dummy data

Our UI is ready but we need to update the winner of each round to achieve the above result. For that we will add more properties to GameSettings:

var currentRound = 0func setCurrentRound() {
currentRound = rounds.filter({$0.winnerPlayer == -1}).first?.round ?? 0
}
func updateRoundDetails() {
var _ = rounds.filter { round in
if round.round == currentRound {
round.winnerPlayer = winner
}
return true
}
}

setCurrentRound() will be called on onAppear() of GameScreen. And whenever a winner is decided or the game is ended with a tie, we will call updateRoundDetails() for setting the winner of the current round.

We need to restrict the players to play after 3 rounds. For that, we will add a new property isAllRoundPlayed inside GameSettings which will return true or false as:

var isAllRoundPlayed: Bool {
return rounds.filter({$0.winnerPlayer != -1}).count == 3
}

This flag will be used on the “Play Game” button to decide the disability as:

...
.disabled(settings.isAllRoundPlayed)
...

So whenever a player plays all three rounds, this button becomes greyed out and no operation will be performed. but if you go back to DashboardView and come back again then all the data will be there. We need to reset GameSettings at some point in the app.

PlayButtonView’s onDisappear() gets called whenever a new view is pushed or it gets popped, so resetting the setting here is not a good choice. Another option is to use DashboardView’s onAppear() as it will be called when PlayButtonView is popped.

...
.onAppear(){
settings.resetGame()
}
...

Do check out my other articles in this series:

Text view styling

Button

Navigation

Shapes

Drawing

Data Flow

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