简体   繁体   中英

Escaping closure captures mutating 'self' parameter Error in Swift

Creating a simple card game (Set) and I have a function in the model that deals X cards onto the deck. Currently, when I click the deal card button they all show up at once so I added the timer so that they would appear one after another. This gives the error "Escaping closure captures mutating 'self' parameter" Any ideas on what I can fix?

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

A timer is not even required. You can use transitions in combination with an animation to get the desired effect. Here the transition is delayed based on the index of the card:

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

Or a bit simpler (without the 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)
    }
}

Note this approach works fine if you're centering the cards, because the previous cards will need to shift too. If you left-align the cards however (using a spacer) the animation delay will be present for the cards that do not need to shift (the animation starts with an awkward delay). If you need to account for this case, you'll need to make the newly inserted index part of the model.

(This may be duplicating what Jack Goossen has written; go look there first, and if it's not clear, this may give some more explanation.)

The core problem here is that you appear to be treating a struct as a reference type. A struct is a value. That means that each holder of it has its own copy. If you pass a value to a Timer, then the Timer is mutating its own copy of that value, which can't be self .

In SwiftUI, models are typically reference types (classes). They represent an identifiable "thing" that can be observed and changes over time. Changing this type to a class would likely address your problem. (See Jack Goossen's answer, which uses a class to hold the cards.)

This is backwards of the direction that Swift had been moving in with UIKit, where views were reference types and the model was encouraged to be made of value types. In SwiftUI, views are structs, and the model is usually made of classes.

(Using Combine with SwiftUI, it's possible to make both view and model into value types, but that's possibly beyond what you were trying to do here, and is a bit more complex if you haven't studied Combine or reactive programming already.)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM