0

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

Rob
  • 415,655
  • 72
  • 787
  • 1,044
Michael
  • 616
  • 5
  • 20
  • 1
    It depends on the context of the code. A `@State` property is usually inside a SwiftUI `View` which does run on the main actor. – vadian Mar 15 '22 at 11:49
  • I've attached my code. It is a normal SwiftUI View. So I think it should be in the main actor?! – Michael Mar 15 '22 at 11:51
  • I was on the wrong path, not the @State property causes the problem, it is the delay function. this is in embedded in a class witch runs as a main actor... – Michael Mar 15 '22 at 12:04
  • 1
    Watch [this video](https://developer.apple.com/videos/play/wwdc2021/10133/), it explains your issue. – HunterLion Mar 15 '22 at 12:09
  • Thanks, I will look this video. For the moment I solved my problem by moving the delay() from the class to the view. at least a workaround... – Michael Mar 15 '22 at 12:11
  • 1
    You need to understand that your code was always wrong, but Apple is only now starting to enforce the real rules. Keep in mind that concurrency is a work in progress! – matt Mar 15 '22 at 12:57
  • Sure, in know that I have to dive deeper :-) – Michael Mar 15 '22 at 15:29

1 Answers1

1

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.

Rob
  • 415,655
  • 72
  • 787
  • 1,044