转义闭包捕获 Swift 中的变异“self”参数错误

Posted

技术标签:

【中文标题】转义闭包捕获 Swift 中的变异“self”参数错误【英文标题】:Escaping closure captures mutating 'self' parameter Error in Swift 【发布时间】:2020-06-28 22:37:14 【问题描述】:

创建一个简单的纸牌游戏(Set),我在模型中有一个函数可以将 X 牌发到牌组上。目前,当我单击交易卡按钮时,它们都会同时显示,因此我添加了计时器,以便它们一个接一个地出现。这给出了错误“转义闭包捕获变异'self'参数”关于我可以修复什么的任何想法?

mutating func deal(_ numberOfCards: Int) 
        for i in 0..<numberOfCards 
            Timer.scheduledTimer(withTimeInterval: 0.3 * Double(i), repeats: false)  _ in
                if deck.count > 0 
                    dealtCards.append(deck.removeFirst())
                
            
            

【问题讨论】:

您应该仅在更新 UI 时使用计时器。做你的洗牌/交易,一旦你完成了调用一个只更新 UI 的方法 我会使用 UIView 动画而不是计时器 "让它们一个接一个地出现" 这是视图的工作,而不是模型的工作。模型应该保持不变。将计时器代码移动到您的视图中。 @Sweeper 这就是我想要做的,但我发现这样做的唯一方法是让 View 执行 ForLoop,这会在代码的后面产生问题。有没有办法让模型中的交易函数将所有 X 卡添加到数组中,然后视图将每张卡一张一张地添加? @LeoDabus 如果通过将卡片添加到模型中的数组来完成发牌和洗牌,我如何使它们全部单独动画。除非从视图中调用 dealcard 12。 【参考方案1】:

甚至不需要计时器。您可以将过渡与动画结合使用以获得所需的效果。这里的过渡是根据卡片的索引延迟的:

class CardModel: ObservableObject 
    @Published var cards: [Int] = []
    
    func deal(_ numberOfCards: Int) 
        cards += (cards.count ..< (cards.count + numberOfCards)).map  $0 
    
    
    func clear() 
        cards = []
    


struct ContentView: View 

    @ObservedObject var cardModel = CardModel()
    
    var body: some View 
        
        GeometryReader  geometry in
            VStack 
                HStack 
                    ForEach(0 ..< self.cardModel.cards.count, id: \.self)  index in
                        CardView(cardNumber: self.cardModel.cards[index])
                            .transition(.offset(x: geometry.size.width))
                            .animation(Animation.easeOut.delay(Double(index) * 0.1))
                    
                
                
                Button(action:  self.cardModel.deal(2) ) 
                    Text("Deal")
                
                
                Button(action:  self.cardModel.clear() ) 
                    Text("Clear")
                
            
        
    


struct CardView: View 
    let cardNumber: Int
    
    var body: some View 
        Rectangle()
            .foregroundColor(.green)
            .frame(width: 9, height: 16)
    

或者更简单一点(没有 CardModel):

struct ContentView: View 

    @State var cards: [Int] = []
    
    func deal(_ numberOfCards: Int) 
        cards += (cards.count ..< (cards.count + numberOfCards)).map  $0 
    
    
    func clear() 
        cards = []
    
    
    var body: some View 
        
        GeometryReader  geometry in
            VStack 
                HStack 
                    ForEach(0 ..< self.cards.count, id: \.self)  index in
                        CardView(cardNumber: self.cards[index])
                            .transition(.offset(x: geometry.size.width))
                            .animation(Animation.easeOut.delay(Double(index) * 0.1))
                    
                
                
                Button(action:  self.deal(2) ) 
                    Text("Deal")
                
                
                Button(action:  self.clear() ) 
                    Text("Clear")
                
            
        
    


struct CardView: View 
    let cardNumber: Int
    
    var body: some View 
        Rectangle()
            .foregroundColor(.green)
            .frame(width: 9, height: 16)
    

注意如果您将卡片居中,这种方法可以正常工作,因为之前的卡片也需要移动。但是,如果您将卡片左对齐(使用间隔),则不需要移动的卡片会出现动画延迟(动画以尴尬的延迟开始)。如果您需要考虑这种情况,则需要使新插入的索引成为模型的一部分。

【讨论】:

这与原始代码有点不同。原来的deal(_:) 允许您将卡片添加到现有手牌中。这个版本总是用给定尺寸的全新手来替换手。如果我没看错的话,如果你切换到append 而不是替换,我认为这种动画方法不会起作用。 @RobNapier 不错。我刚试过:它有点适用于追加,但我不确定效果是否是 OP 想要的。 @RobNapier 我同意可识别结构是 ForEach 循环中更好的方法,但我仍然需要索引来计算动画延迟。 (不需要最后的评论,id 参数处理)【参考方案2】:

(这可能是在重复 Jack Goossen 写的内容;先去看看,如果不清楚,这可能会给出更多解释。)

这里的核心问题是您似乎将结构视为引用类型。结构是一个值。这意味着它的每个持有者都有自己的副本。如果您将值传递给 Timer,则 Timer 正在改变它自己的该值的副本,它不能是 self

在 SwiftUI 中,模型通常是引用类型(类)。它们代表了一种可识别的“事物”,可以观察到并随着时间的推移而变化。将此类型更改为类可能会解决您的问题。 (请参阅 Jack Goossen 的答案,它使用一个类来持有卡片。)

这与 Swift 与 UIKit 的发展方向背道而驰,在 UIKit 中,视图是引用类型,并且鼓励模型由值类型构成。在 SwiftUI 中,视图是结构体,模型通常由类组成。

(使用 Combine 和 SwiftUI,可以将 view 和 model 都变成值类型,但这可能超出了您在这里尝试做的事情,如果您还没有学习过 combine 或响应式编程,那就有点复杂了.)

【讨论】:

非常有见地。只是为了挑剔:您也可以在 ForEach 循环中使用带有值类型的动画方法,只要它们是可识别的(我的答案中的索引)。使用 ObservedObject iso 状态更多的是一种习惯,而不是有意的选择。 ForEach 循环可以超过值类型,但被观察的对象必须是一个类(这是应用append 的东西)。

以上是关于转义闭包捕获 Swift 中的变异“self”参数错误的主要内容,如果未能解决你的问题,请参考以下文章

错误:转义闭包捕获变异的“自我”参数

SwiftUI 转义闭包捕获变异的“自我”参数

获取 JSON,附加到数组:转义闭包捕获变异的“自我”参数

在 Swift 3.0 中的转义闭包中改变自我(结构/枚举)

如何解决转义闭包捕获 Swift 中的“inout”参数?

Swift 5:转义闭包捕获'inout'参数