Why can't I mutate a variable initially set to

2020-02-14 19:52发布

问题:

GOAL: I'm trying to make a general struct that can take an array of Ints and go through and set a timer for each one (and show a screen) in succession.

Problem: I get Escaping closure captures mutating 'self' parameter error as shown in the code.

import SwiftUI

struct ContentView: View {

    @State private var timeLeft = 10
    @State private var timers = Timers(timersIWant: [6, 8, 14])
//    var timersIWantToShow: [Int] = [6, 8, 14]

    var body: some View {
        Button(action: {self.timers.startTimer(with: self.timeLeft)}) {
            VStack {
                Text("Hello, World! \(timeLeft)")
                    .foregroundColor(.white)
                    .background(Color.blue)
                    .font(.largeTitle)
            }
        }
    }

    struct Timers {

        var countDownTimeStart: Int = 0
        var currentTimer = 0
        var timersIWant: [Int]

        mutating func startTimer(with countDownTime: Int) {

            var timeLeft = countDownTime

            Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { timer in  //Escaping closure captures mutating 'self' parameter


                if timeLeft > 0 {
                    timeLeft -= 1
                } else {
                    timer.invalidate()
                    self.currentTimer += 1
                    if self.currentTimer < self.timersIWant.count {
                        self.startTimer(with: self.timersIWant[self.currentTimer])
                    } else {
                        timer.invalidate()
                    }
                }
            })
        }
    }

}

I'm not sure if this has to do with my recursvie function (maybe this is bad form?) and I'm guessing the escaping closure is the func startTimer and the offending the 'self' parameter is the countDownTime parameter, but I'm not really sure what is happening or why it's wrong.

回答1:

As Gil notes, this needs to be a class because you are treating it as a reference type. When you modify currentTimer, you don't expect that to create a completely new Timers instance, which is what happens with a value type (struct). You expect it to modify the existing Timers instance. That's a reference type (class). But to make this work, there's quite a bit more you need. You need to tie the Timers to the View, or the View won't update.

IMO, the best way to approach this is let Timers track the current timeLeft and have the view observe it. I've also added an isRunning published value so that the view can reconfigure itself based on that.

struct TimerView: View {

    // Observe timers so that when it publishes changes, the view is re-rendered
    @ObservedObject var timers = Timers(intervals: [10, 6, 8, 14])

    var body: some View {
        Button(action: { self.timers.startTimer()} ) {
            Text("Hello, World! \(timers.timeLeft)")
            .foregroundColor(.white)
            .background(timers.isRunning ? Color.red : Color.blue) // Style based on isRunning
            .font(.largeTitle)
        }
        .disabled(timers.isRunning)   // Auto-disable while running
    }
}

// Timers is observable
class Timers: ObservableObject {

    // And it publishes timeLeft and isRunning; when these change, update the observer
    @Published var timeLeft: Int = 0
    @Published var isRunning: Bool = false

    // This is `let` to get rid of any confusion around what to do if it were changed.
    let intervals: [Int]

    // And a bit of bookkeeping so we can invalidate the timer when needed
    private var timer: Timer?

    init(intervals: [Int]) {
        // Initialize timeLeft so that it shows the upcoming time before starting
        self.timeLeft = intervals.first ?? 0
        self.intervals = intervals
    }

    func startTimer() {
        // Invalidate the old timer and stop running, in case we return early
        timer?.invalidate()
        isRunning = false

        // Turn intervals into a slice to make popFirst() easy
        // This value is local to this function, and is captured by the timer callback
        var timerLengths = intervals[...]

        guard let firstInterval = timerLengths.popFirst() else { return }

        // This might feel redundant with init, but remember we may have been restarted
        timeLeft = firstInterval

        isRunning = true

        // Keep track of the timer to invalidate it elsewhere.
        // Make self weak so that the Timers can be discarded and it'll clean itself up the next
        // time it fires.
        timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] timer in
            guard let self = self else {
                timer.invalidate()
                return
            }

            // Decrement the timer, or pull the nextInterval from the slice, or stop
            if self.timeLeft > 0 {
                self.timeLeft -= 1
            } else if let nextInterval = timerLengths.popFirst() {
                self.timeLeft = nextInterval
            } else {
                timer.invalidate()
                self.isRunning = false
            }
        }
    }
}


回答2:

Escaping closure captures mutating 'self' parameter

The escaping closure is the Button's action parameter, and the mutating function is your startTimer function.

        Button(action: {self.timers.startTimer(with: self.timeLeft)}) {

A simple solution is to change Times to be a class instead of a struct.

Also notice that timeLeft is defined in two places. I don't think this is what you want.