I'm very new to SwiftUI, and I'm working on a TicTacToe board for my class. I'm following this article on Medium, but I've encountered a problem.
The squares don't activate as you play. It's not until the game is finished that you can see where the moves were made. I don't understand why this is happening or how to fix it. Any help would be much appreciated!
import SwiftUI
import Combine
enum SquareStatus {
case empty
case visitor
case home
}
class Square: ObservableObject {
let didChange = PassthroughSubject<Void, Never>()
var status: SquareStatus {
didSet {
didChange.send(())
}
}
init(status: SquareStatus) {
self.status = status
}
}
class ModelBoard {
var squares = [Square]()
init() {
for _ in 0...8 {
squares.append(Square(status: .empty))
}
}
func resetGame() {
for i in 0...8 {
squares[i].status = .empty
}
}
var gameOver: (SquareStatus, Bool) {
get {
if thereIsAWinner != .empty {
return (thereIsAWinner, true)
} else {
for i in 0...8 {
if squares[i].status == .empty {
return (.empty, false)
}
}
return (.empty, true)
}
}
}
private var thereIsAWinner:SquareStatus {
get {
if let check = self.checkIndexes([0, 1, 2]) {
return check
} else if let check = self.checkIndexes([3, 4, 5]) {
return check
} else if let check = self.checkIndexes([6, 7, 8]) {
return check
} else if let check = self.checkIndexes([0, 3, 6]) {
return check
} else if let check = self.checkIndexes([1, 4, 7]) {
return check
} else if let check = self.checkIndexes([2, 5, 8]) {
return check
} else if let check = self.checkIndexes([0, 4, 8]) {
return check
} else if let check = self.checkIndexes([2, 4, 6]) {
return check
}
return .empty
}
}
private func checkIndexes(_ indexes: [Int]) -> SquareStatus? {
var homeCounter:Int = 0
var visitorCounter:Int = 0
for anIndex in indexes {
let aSquare = squares[anIndex]
if aSquare.status == .home {
homeCounter = homeCounter + 1
} else if aSquare.status == .visitor {
visitorCounter = visitorCounter + 1
}
}
if homeCounter == 3 {
return .home
} else if visitorCounter == 3 {
return .visitor
}
return nil
}
private func aiMove() {
var anIndex = Int.random(in: 0 ... 8)
while (makeMove(index: anIndex, player: .visitor) == false && gameOver.1 == false) {
anIndex = Int.random(in: 0 ... 8)
}
}
func makeMove(index: Int, player:SquareStatus) -> Bool {
if squares[index].status == .empty {
squares[index].status = player
if player == .home { aiMove() }
return true
}
return false
}
}
struct SquareView: View {
@ObservedObject var dataSource:Square
var action: () -> Void
var body: some View {
Button(action: {
self.action()
}) {
Text((dataSource.status != .empty) ?
(dataSource.status != .visitor) ? "X" : "0"
: " ")
.font(.largeTitle)
.foregroundColor(Color.black)
.frame(minWidth: 60, minHeight: 60)
.background(Color.gray)
.padding(EdgeInsets(top: 4, leading: 4, bottom: 4, trailing: 4))
}
}
}
struct ContentView : View {
private var checker = ModelBoard()
@State private var isGameOver = false
func buttonAction(_ index: Int) {
_ = self.checker.makeMove(index: index, player: .home)
self.isGameOver = self.checker.gameOver.1
}
var body: some View {
VStack {
HStack {
SquareView(dataSource: checker.squares[0]) { self.buttonAction(0) }
SquareView(dataSource: checker.squares[1]) { self.buttonAction(1) }
SquareView(dataSource: checker.squares[2]) { self.buttonAction(2) }
}
HStack {
SquareView(dataSource: checker.squares[3]) { self.buttonAction(3) }
SquareView(dataSource: checker.squares[4]) { self.buttonAction(4) }
SquareView(dataSource: checker.squares[5]) { self.buttonAction(5) }
}
HStack {
SquareView(dataSource: checker.squares[6]) { self.buttonAction(6) }
SquareView(dataSource: checker.squares[7]) { self.buttonAction(7) }
SquareView(dataSource: checker.squares[8]) { self.buttonAction(8) }
}
}
.alert(isPresented: $isGameOver) {
Alert(title: Text("Game Over"),
message: Text(self.checker.gameOver.0 != .empty ?
(self.checker.gameOver.0 == .home) ? "You Win!" : "iPhone Wins!"
: "Parity"), dismissButton: Alert.Button.destructive(Text("Ok"), action: {
self.checker.resetGame()
}) )
}
}
}
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
You probably want to pass your View Model into the view hierarchy as an Environment Object instead of hard coding the construction of the VM in the View. I cleaned up the code a bit. The following code works fine on XCode 12.5.
I couldn't get @NikzJon version or Costantino Pistagna's version to work on the latest XCode.
The full project can be found at GitHub
// ContentView.swift
import SwiftUI
enum Owner
{
case vacant
case naught
case cross
var show: String
{
get {
switch self
{
case .vacant:
return " "
case .naught:
return "0"
case .cross:
return "X"
}
}
}
var showWinnerAsCross : String
{
switch self
{
case .vacant:
return "Draw!"
case .naught:
return "Lose!"
case .cross:
return "Win!"
}
}
var showWinnerAsNaught : String
{
switch self
{
case .vacant:
return "Draw!"
case .cross:
return "Lose!"
case .naught:
return "Win!"
}
}
static func blankSquares(_ n: Int) -> [Owner]
{
return [Owner](repeating: .vacant, count: n)
}
}
enum Slant
{
case forward
case back
}
enum WinCondition
{
case row (y:Int)
case col (x: Int)
case diag (slant: Slant)
static let rows : [WinCondition] = Array((0...2).map { row(y: $0)})
static let cols : [WinCondition] = Array((0...2).map { col(x: $0)})
static let diags : [WinCondition] = [diag(slant: .forward), diag(slant: .back)]
static let all : [WinCondition] = rows + cols + diags
var line : [Int] { get {
switch self{
case .row(y: let y) :
return [3*y+0,3*y+1,3*y+2]
case .col(x: let x):
return [x+0,x+3,x+6]
case .diag(slant: let slant):
return slant == .forward ? [0,4,8] : [2,4,6]
}
}
}
}
extension Array where Element == Owner
{
var free: [Int] { get
{
self
.enumerated()
.filter { $0.element == .vacant }
.map { $0.offset }
}
}
var randomFreeIndex: Int { get
{
let moves = self.free
let move = Int.random(in: 0..<moves.count)
return moves[move]
}
}
var isFull: Bool {
get
{
return self.free.isEmpty
}
}
}
class GameBoard : ObservableObject
{
@Published var squares = Owner.blankSquares( 9)
var currentPlayer = Owner.cross
var computerPlayer = Owner.naught
func reset() {
squares = Owner.blankSquares( 9)
}
func hasWon(player: Owner) -> Bool {
return WinCondition.all.contains { $0.line.allSatisfy { squares[$0] == player }}
}
var winner : Owner?
{
get {
if hasWon(player: .cross)
{
return .cross
}
else if hasWon(player: .naught)
{
return .naught
}
else if squares.free.isEmpty
{
return .vacant
}
return nil
}
}
@Published var isOver : Bool = false;
func checkIsOver()
{
if winner != nil
{
isOver = true
}
}
var showWinner : String
{
get{
return "You " + (winner?.showWinnerAsCross ?? " ")
}
}
func randomMove(player: Owner) {
if squares.isFull
{
return
}
squares[squares.randomFreeIndex] = player
}
func turn(squareIndex:Int) {
if squares[squareIndex] != .vacant
{
return
}
squares[squareIndex] = currentPlayer
checkIsOver()
randomMove(player: computerPlayer)
checkIsOver()
}
}
struct SquareView: View {
@EnvironmentObject var board: GameBoard
var index: Int
var body: some View {
Button(action: {
board.turn(squareIndex: index)
}) {
Text( board.squares[index].show)
.foregroundColor(Color.white)
.font(.largeTitle)
.frame(minWidth: 80, minHeight: 80)
.background(Color.blue)
.padding(EdgeInsets(top: 3, leading: 3, bottom: 3, trailing: 3))
}
}
}
struct ContentView: View {
@EnvironmentObject var board: GameBoard
var body: some View {
VStack {
HStack {
SquareView(index:0)
SquareView(index:1)
SquareView(index:2)
}
HStack {
SquareView(index:3)
SquareView(index:4)
SquareView(index:5)
}
HStack {
SquareView(index:6)
SquareView(index:7)
SquareView(index:8)
}
}
.alert(isPresented: $board.isOver) {
Alert(title: Text( board.showWinner) , dismissButton: Alert.Button.destructive(Text("Try Again"), action:
board.reset
) )
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
// tictactoeApp.swift
import SwiftUI
@main
struct tictactoeApp: App {
@StateObject var board = GameBoard()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(board)
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With