Search code examples
iosswiftswiftuiasync-awaitswift-concurrency

Reference to captured var in concurrently-executing code


I was trying out the new async/await pattern in swift and I came accross something which I found it confusing.

struct ContentView: View {
    var body: some View  {
        Text("Hello World!")
            .task {
                var num = 1

                Task {
                    print(num)
                }

                Task {
                    print(num)
                }
            }
    }

    func printScore() async {
        var score = 1

        Task { print(score) }
        Task { print(score) }
    }
}

enter image description here

Can someone please clarify looking at the above screenshot on why the compiler only complaints about captured var inside printScore() function and does not complaint when the same is being done using the task modifier on the body computed property of the ContentView struct (i.e Line 14-24) ?

This is the example I came up with and got confused on the compiler behavior.I also change the compiler setting "Strict Concurrency Checking” build setting to “Complete” and still don't see the compiler complaining.


Solution

  • This is a special power of @MainActor. In a View, body is tagged MainActor:

    @ViewBuilder @MainActor var body: Self.Body { get }
    

    Tasks inherit the context of their caller, so those are are also MainActor. If you replace Task with Task.detached in body, you will see the same error, since this will move the Task out of the MainActor context.

    Conversely, if you add @MainActor to printScore it will also compile without errors:

    @MainActor func printScore() async {
        var score = 1
    
        Task { print(score) }
        Task { print(score) }
    }
    

    score inherits the MainActor designation, and is protected. There is no concurrent access to score, so this is fine and correct.

    This actually applies to all actors, but I believe the compiler has a bug that makes things not quite behave the way you expect. The following code compiles:

    actor A {
        var actorVar = 1
        func capture() {
            var localVar = 1
            Task {
                print(actorVar)
                print(localVar)
            }
        }
    }
    

    However, if you remove the reference to actorVar, the closure passed to Task will not be put into the actor's context, and that will make the reference to localVar invalid. IMO, this is a compiler bug.