目标:我正在尝试制作一个通用结构,该结构可以采用一个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 closure
是func startTimer
,而令人讨厌的'self' parameter
是countDownTime
参数,但我不太确定发生了什么或为什么会出错。 最佳答案
正如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
}
}
}
}