Consider this code that places fluctuating CatView
s in a view's .background
modifier:
@State private var catOn = true
...
SomeView()
.background {
GeometryReader { geo in
ForEach(0...numCats, id: \.self) { n in
CatView(color: n%2 == 0 ? color1 : color2, size: 15, isOn: n%2 == 0 ? catOn : !catOn)
//.frame = ...
}
.task {
while !Task.isCancelled {
try? await Task.sleep(for: .seconds(1.0))
catOn.toggle()
}
}
}
}
The problem is that I think this code is causing a crash sometimes when the view is replaced by another view, perhaps because the task fires during the transition (I don't get the crash when I comment out the .task
). How can I cancel this task when a button is pushed, before I trigger the property that transitions this view away? Also, is there a better way to set this task up? As you can see, I need the CatViews to swap colors every second.
First, you should catch cancellation errors thrown by sleep
so that catOn
is not toggled when the tasks is cancelled during the sleep
:
while !Task.isCancelled {
do {
try await Task.sleep(for: .seconds(1.0))
catOn.toggle()
} catch is CancellationError {
return // the task is cancelled during the sleep
} catch {
fatalError("Something unexpected happened")
}
}
The task will still only be cancelled when the view disappears. To cancel the task immediately on demand, you can use the task(id:)
overload. When this id
changes, the currently running task is cancelled, and a new task is started. You can return
immediately when the new id
is some sentinel value. For example:
@State var cancelled = false
@State var catOn = false
var body: some View {
Toggle("Cat On", isOn: $catOn)
Button("Cancel Task") {
cancelled = true
}
.task(id: cancelled) {
if cancelled { return }
while !Task.isCancelled {
do {
try await Task.sleep(for: .seconds(1.0))
catOn.toggle()
} catch is CancellationError {
return
} catch {
fatalError("Something unexpected happened")
}
}
}
}