swiftuiswiftui-state

SwiftUI TicTacToe board


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

Solution

  • 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()
                            }) )
            }
        }
    }