我正在学习Swift,并试图开发第一款应用-Tic Tac Toe.我想同时实现PVP和AI,并坚持显示谁是下一个移动的(X/O).AI模式(目前只是随机模式)运行良好,PVP也运行良好,但它总是显示"X to move".如果我在TicTacToe文件中将变量playerToMove设置为@Published,这样文本就可以更新,应用程序就不能工作.内容视图显示所有内容,但我无法单击任何内容.如果我开发这个应用程序,它只是一个白屏.

下面我显示的是应该更新的文本:

struct ContentView: View {
@ObservedObject var ticTacToe = TicTacToeModel()

var currPlayer: String {
    return self.ticTacToe.playerToMove == false ? "X" : "O"
}

var body: some View {
    Text(self.selection == true ? "\(currPlayer) to move" : " ")
}

以下是TicTacToeModel:

class TicTacToeModel: ObservableObject {
var playerToMove : Bool = false // App breaks when set to @Published
}

这里有完整的源代码:(也可以在https://pastebin.com/srDNZHnU下载)

// ComponentView:
 
import SwiftUI
import Combine
 
struct ContentView: View {
    @Environment(\.colorScheme) var colorScheme
    @ObservedObject var ticTacToe = TicTacToeModel()
    @State var selection : Bool = false
    @State var move : Bool = false
    @State var gameOver : Bool = false
    
    func buttonAction(_ index : Int) {
        if self.ticTacToe.makeMove(index: index, gameType: selection) {
            self.gameOver = self.ticTacToe.gameOver.1
        }
    }
    
    var currPlayer: String {
        print("runned", self.ticTacToe.playerToMove)
        return self.ticTacToe.playerToMove == false ? "X" : "O"
    }
    
    var body: some View {
        VStack {
            Picker(selection: $selection, label: Text("Game")) {
                Text("AI").tag(false)
                Text("PVP").tag(true)
            }
            .pickerStyle(SegmentedPickerStyle())
            .padding(.horizontal, 89)
            .onReceive(Just(selection)) { _ in
                self.ticTacToe.resetGame()
            }
            
            Spacer()
            
            Text(self.selection == false ? "Tic Tac Toe - AI" : "Tic Tac Toe - PVP")
                .bold()
                .font(.title)
                //.padding(.bottom)
                .foregroundColor(colorScheme == .dark ? Color.white.opacity(0.7) : Color.black.opacity(0.7))
            
            Text(self.selection == true ? "\(currPlayer) to move" : " ")
                .bold()
                .font(.title2)
                .padding(.bottom)
                .foregroundColor(colorScheme == .dark ? Color.white.opacity(0.7) : Color.black.opacity(0.7))
            
            ForEach(0 ..< ticTacToe.squares.count / 3, id: \.self, content: { row in
                HStack {
                    ForEach(0 ..< 3, content: { column in
                        let index = row * 3 + column
                        SquareView(dataSouce: ticTacToe.squares[index], action: { self.buttonAction(index) })
                    })
                }
            })
            
            Spacer()
            
            Button(action: {
                self.ticTacToe.resetGame()
            }, label: {
                Text("Reset")
                    .foregroundColor(Color.red.opacity(0.7))
            })
        }.alert(isPresented: self.$gameOver, content: {
            Alert(title: Text("Game Over"),
                  message: Text(self.ticTacToe.gameOver.0 != .empty ?
                                self.ticTacToe.gameOver.0 == .x ? "You Won!" : "AI Won!" : "Draw!"),
                  dismissButton: Alert.Button.destructive(Text("Ok"), action: {
                self.ticTacToe.resetGame()
            }))
        })
    }
}
 
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
 
// ---------------------------------------------------------------------
// TicTacToe:
 
import Foundation
 
class TicTacToeModel: ObservableObject {
    @Published var squares = [Square]()
    var playerToMove : Bool = false // App breaks when set to @Published
    
    init() {
        for _ in 0...8 {
            squares.append(Square(status: .empty))
        }
    }
    
    func resetGame() {
        for i in 0...8 {
            squares[i].squareStatus = .empty
            playerToMove = false
        }
    }
    
    var gameOver : (SquareStatus, Bool) {
        get {
            if winner != .empty {
                return (winner, true)
            } else {
                for i in 0...8 {
                    if squares[i].squareStatus == .empty {
                        return (.empty, false)
                    }
                }
                return (.empty, true)
            }
        }
    }
    
    func makeMove(index: Int, gameType: Bool) -> Bool {
        var player: SquareStatus
        if playerToMove == false {
            player = .x
        } else {
            player = .o
        }
        if squares[index].squareStatus == .empty {
            squares[index].squareStatus = player
            if playerToMove == false && gameType == false {
                moveAI()
                playerToMove = true
            }
            playerToMove.toggle()
            return true
        }
        return false
    }
    
    private func moveAI() {
        var index = Int.random(in: 0...8)
        playerToMove = true
        while makeMove(index: index, gameType: true) == false && gameOver.1 == false {
            index = Int.random(in: 0...8)
        }
    }
    
    private var winner: 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 xCount : Int = 0
        var oCount : Int = 0
        for index in indexes {
            let square = squares[index]
            if square.squareStatus == .x {
                xCount += 1
            } else if square.squareStatus == .o {
                oCount += 1
            }
        }
        if xCount == 3 {
            return .x
        } else if oCount == 3 {
            return .o
        }
        return nil
    }
}
 
// ---------------------------------------------------------------------
// SquareView:
 
import Foundation
import SwiftUI
 
enum SquareStatus {
    case empty
    case x
    case o
}
 
class Square : ObservableObject {
    @Published var squareStatus : SquareStatus
    
    init(status: SquareStatus) {
        self.squareStatus = status
    }
}
 
struct SquareView : View {
    @Environment(\.colorScheme) var colorScheme
    @ObservedObject var dataSouce : Square
    var action: () -> Void
    var body: some View {
        Button(action: {
            self.action()
        }, label: {
            Text(self.dataSouce.squareStatus == .x ?
                 "X" : self.dataSouce.squareStatus == .o ? "O" : " ")
            .font(.system(size: 60))
            .bold()
            .foregroundColor(colorScheme == .dark ? Color.white.opacity(0.9) : Color.black.opacity(0.9))
            .frame(width: 90, height: 90, alignment: .center)
            .background(colorScheme == .dark ? Color.white.opacity(0.3).cornerRadius(10) : Color.gray.opacity(0.3).cornerRadius(10))
            .padding(4)
        })
    }
}

我原以为如果向该变量加@Published,"下一步"就能正常工作,但我的代码没有出错.我try 使用@Published创建新的var进行测试,它一直在运行,直到我在任何函数中将其设置为不同的值.

推荐答案

问题出在你的Picker美元.更具体地说,.onReceive.它在导致self.ticTacToe.resetGame()运行的视图重绘的任何时候激活,该更新playerToMove触发了视图重绘...你会有一个无限循环.

Picker(selection: $selection, label: Text("Game")) {
    Text("AI").tag(false)
    Text("PVP").tag(true)
}
.pickerStyle(SegmentedPickerStyle())
.padding(.horizontal, 89)
.onReceive(Just(selection)) { _ in
    self.ticTacToe.resetGame()
}

.onReceive更改为onChange(of:):

.onChange(of: selection) { _ in
    self.ticTacToe.resetGame()
}

只有当selection实际更改时才会激活.


Note:

您必须:

@ObservedObject var ticTacToe = TicTacToeModel()

在你的ContentView美元里.既然这是你的模型的真理来源,它应该是:

@StateObject var ticTacToe = TicTacToeModel()

根据一般经验,如果要为对象指定初始值,则使用@StateObject.如果对象从调用视图传入其值,则使用@ObservedObject.

Swift相关问答推荐

为什么Swift在某些链调用中不能对不可变值使用变异成员,而在其他链调用中则不能使用变异成员?

如何在 Vapor 中制作可选的查询过滤器

如何增加 NSStatusBar 图像大小?

如何使用可搜索在搜索栏中搜索包括>或<?

我如何在 swift 中的 viewdidload 之前进行委托?

使用索引来访问 libc 定义的元组

为什么自定义串行队列的目标是全局队列时,Swift中不会并发执行?

在Equatable上引用运算符函数==要求Dictionary.Values符合Equatable

如何修改下面的代码,使图像 Select 的顺序与使用快速并发的图像呈现的顺序相匹配?

为 ObservedObject 和 State 对象配置预览

在主要参与者中启动分离任务和调用非隔离函数之间的区别

在 RealityKit 中创建转场

SwiftUI 中的计时器崩溃屏幕

如何在 RxSwift 中获取 controlEvent 类型?

VStack SwiftUI 中的动态内容高度

Swift 覆盖实例变量

Swift中switch 盒的详尽条件

Swift 3:小数到 Int

在 Swift 中指定 UITextField 的边框半径

使用 Swift 以编程方式自定义 UITableViewCell