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