6

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
alison
  • 63
  • 2

2 Answers2

4

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)
        }
    }
}
Geoff Burns
  • 525
  • 2
  • 7
  • 19
0

There are small tweaks through out the code. It is basically fault in the data structure and how they communicate. Compare it with your code and see the difference. Also aiMove is not really AI. Actually it is a random moves instead of minimax algorithm.

import SwiftUI
import Combine

enum SquareStatus {
    case empty
    case visitor
    case home
}

struct Square {
    var status: SquareStatus
}

class ModelBoard: ObservableObject {
    @Published 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 {
            var square = squares[index]
            square.status = player
            squares[index] = square
            if player == .home { aiMove() }
            return true
        }
        return false
    }
}



struct SquareView: View {
    var dataSource: Square
    var action: () -> Void
    var body: some View {
        Button(action: {
            print(self.dataSource.status)
            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 MainBoard: View {
    @ObservedObject 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()
                        }) )
        }
    }
}
NikzJon
  • 912
  • 7
  • 25