Search code examples
swiftswiftuiswift-concurrency

Xcode 13.3 new error message with @MainActor


I updated today to Xcode 13.3 and now my code can't compile anymore.

I get two error messages. It seems there is a connection between the errors.

  1. First error:

    Property 'startAnimation' isolated to global actor 'MainActor' can not be mutated from a non-isolated context

    in this line

    kuzorra.delay(interval: 1.5) {startAnimation.toggle()
    
  2. Second error:

    Mutation of this property is only permitted within the actor

    in this line

    @State private var startAnimation = false
    

I don't really understand how to fix these errors. Any help or hints are welcome :-)

For better understanding my view:

struct AnimationView: View {
    
    @EnvironmentObject var kuzorra: Kuzorra
    @AppStorage ("isSoundEnabled") var isSoundEnabled: Bool = true
    @State private var startAnimation = false
    var label: Bool
    var correctAnswer: String
    
    var body: some View {
        VStack {
            Text(label ? "RICHTIG!" : "FALSCH!")
                .foregroundColor(.white)
                .font(.largeTitle)
                .fontWeight(.bold)
                .padding(.horizontal)
                .padding(.top)
            
            
            if !label{ Text("Richtige Antwort: \(correctAnswer)")
                    .font(.caption)
                    .lineLimit(1)
                    .minimumScaleFactor(0.01)
                    .foregroundColor(.white)
                    .padding(.horizontal)
            }
            VStack(alignment: .trailing){
                
                Text(" - Weiter - ")
                    .padding(.bottom, 5)
                    .foregroundColor(.white)
                    .font(.caption2)
            }
        }
        .bgStyle()
        .rectangleStyle()
        .padding()
        .opacity( startAnimation ? 1 : 0)
        .rotationEffect(.degrees(startAnimation ? 2880 : 0))
        .scaleEffect(startAnimation ? 2 : 1/32)
        .animation(Animation.easeOut.speed(1/4), value: startAnimation)
        .onTapGesture {
            kuzorra.currentPage = .page3
        }
        .onAppear {
            if isSoundEnabled   {   AudioServicesPlayAlertSound(SystemSoundID(1320))
            }
            kuzorra.delay(interval: 1.5) {startAnimation.toggle()
            }
        }
    }
}

and here is the code for delay:

@MainActor
class Kuzorra: ObservableObject {

.
.
.

func delay(interval: TimeInterval, closure: @escaping () -> Void) {
        DispatchQueue.main.asyncAfter(deadline: .now() + interval, execute: closure)
    }

There I get the following error message:

Passing non-sendable parameter 'closure' to function expecting a @Sendable closure Parameter 'closure' is implicitly non-sendable.

After pressing fix button this error message disappears..


Solution

  • It is probably prudent to avoid using GCD’s asyncAfter within Swift concurrency codebase. So, rather than:

    .onAppear {
        if isSoundEnabled {
            AudioServicesPlayAlertSound(SystemSoundID(1320))
        }
        kuzorra.delay(interval: 1.5) {
            startAnimation.toggle()
        }
    }
    

    Consider using the .task view modifier (which takes an async closure, and is also cancelable) and sleep(for:) (which, unlike traditional sleep API, does not block the thread):

    .task {
        if isSoundEnabled {
            AudioServicesPlayAlertSound(SystemSoundID(1320))
        }
        try? await Task.sleep(for: .seconds(1.5))
        if !Task.isCancelled {
            startAnimation.toggle()
        }
    }
    

    That achieves asyncAfter like behavior within Swift concurrency. Needless to say, you could also use do-try-catch pattern:

    .task {
        if isSoundEnabled {
            AudioServicesPlayAlertSound(SystemSoundID(1320))
        }
        do {
            try await Task.sleep(for: .seconds(1.5))
            startAnimation.toggle()
        } catch {
            // print(error)
        }
    }
    

    If you use Task.sleep pattern, you either have to try? and then check isCancelled or try and catch the error.

    But the main point is to avoid asyncAfter in Swift concurrency.