I'm trying to learn the future of Swift Concurrency and understand the concurrency checking that will eventually be enabled by default in Swift 6.
I turned on concurrency checks by adding the flags -Xfrontend -warn-concurrency -Xfrontend -enable-actor-data-race-checks
to see what developing under these checks will be like. I came across a warning about capturing a non-sendable value in a sendable closure, but the best way to resolve the warning was unclear.
struct LoadingView: View {
@State private var dotCount = 0
var body: some View {
Text(String(repeating: ".", count: dotCount + 1))
.onAppear(perform: animateDots)
}
func animateDots() {
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { _ in
dotCount = (dotCount + 1) % 3 // <-- Capture of 'self' with non-sendable type 'LoadingView' in a `@Sendable` closure
}.fire()
}
}
I tried a few different things to get this warning to go away:
- The most obvious solution given the warning was to make
LoadingView
conform toSendable
, but that leads to the warningStored property '_dotCount' of 'Sendable'-conforming struct 'LoadingView' has non-sendable type 'State<Int>'
. You can solve that by makingState
orLoadingView
conform to@unchecked Sendable
. I suppose that might be okay since the documentation forState
says, "You can safely mutate state properties from any thread." But I'm don't know why State doesn't conform toSendable
in the first place. - Another idea I had was to mark
LoadingView
withMainActor
and then wrap the body of my timer closure inTask { @MainActor in ... }
This works, but it doesn't seem ideal to have to wrap everything in an extra Task or first LoadingView to be on the MainActor when maybe it doesn't need to be. - My final ideas was to extract the body of the time closure into a method on LoadingView and mark it with @Sendable. Then I can add the method to the capture list of the timer closure and call it without any warnings appearing.
My final code is:
struct LoadingView: View {
@State private var dotCount = 0
var body: some View {
Text(String(repeating: ".", count: dotCount + 1))
.onAppear {
animateDots()
}
}
func animateDots() {
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [incrementDotCount] _ in
incrementDotCount()
}.fire()
}
@Sendable
func incrementDotCount() {
dotCount = (dotCount + 1) % 3
}
}
This seems like a good solutions, except I'm not sure how it actually works. Why am I allowed to mark a method of a non-sendable type with @Sendable
? It seems like I'm still implicitly capturing self in order to call the @Sendable
method. Am I missing something, or is this possibly a bug with how Swift is checking @Sendable closures?