目标:我正在尝试制作一个通用结构,该结构可以采用一个Ints数组,并依次为每个指针设置一个计时器(并显示一个屏幕)。

问题:如代码所示,出现Escaping closure captures mutating 'self' parameter错误。

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

}


我不确定这是否与我的recursvie函数有关(也许这是错误的形式吗?),我猜想escaping closurefunc startTimer,而令人讨厌的'self' parametercountDownTime参数,但我不太确定发生了什么或为什么会出错。

最佳答案

正如Gil所指出的,这必须是一个类,因为您将其视为引用类型。修改currentTimer时,您不希望创建一个全新的Timers实例,而值类型(结构)会发生这种情况。您希望它修改现有的Timers实例。这是一个引用类型(类)。但是要使这项工作有效,您还需要更多。您需要将计时器绑定到视图,否则视图将不会更新。

IMO,解决此问题的最佳方法是让Timers跟踪当前的timeLeft并让视图进行观察。我还添加了一个isRunning发布的值,以便该视图可以基于此重新配置自身。

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

10-07 14:33