21

I have created a class called "AudioPlaybackManager" which is a MainActor class as a StateObject when the app starts up. Im getting this warning. How do I fix this?

@main
struct Pro_Music_2App: App {
    @StateObject var audioPlaybackManager = AudioPlaybackManager() //This line is giving warning to me
    
    @StateObject var dataController = DataController()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(audioPlaybackManager.manager)
                .environmentObject(audioPlaybackManager)
                .environmentObject(audioPlaybackManager.manager.playlistManager)
                .environmentObject(audioPlaybackManager.musicPlayerViewManager)
                .environmentObject(audioPlaybackManager.scrubberViewModel)
                .environment(\.managedObjectContext, dataController.container.viewContext)
                
        }
    }
}

@MainActor class AudioPlaybackManager: ObservableObject {
    ...
}
SwiftEnthusiast
  • 402
  • 2
  • 10
  • 1
    I'm having trouble with the expression `@MainActor class var AudioPlaybackManager: ObservableObject`. What does `class var` mean here? – matt Mar 08 '22 at 14:27
  • Oops my bad. It was just a typo... – SwiftEnthusiast Mar 08 '22 at 16:01
  • Cool, but you should not be typing at all. Only copy and paste when you put code into Stack Overflow. Otherwise there's a risk of giving us false information by mistake. – matt Mar 08 '22 at 16:22
  • OK so can you solve this by making the particular methods of AudioPlaybackManager `@MainActor` methods, as needed, but _not_ the class as a whole? – matt Mar 08 '22 at 16:23

2 Answers2

26

Edit 2: This issue has been resolved with Swift 5.7 (release notes):

The diagnostic about non-isolated default-value expressions introduced for Swift 5.6 in the Xcode 13.3 release is no longer available. The proposed rule in SE-0327 wasn’t precise enough to avoid flagging an innocuous yet common pattern in SwiftUI code involving @StateObject properties and @MainActor. (88971160)

I assume a more precise rule will be introduced in Swift 6.0.


Edit: As Michael Long mentioned in the comments and I already guessed, this probably won’t be the final solution. Instead, the problem will probably be resolved in the compiler and the existing syntax will work without warning.

Depending on how strict you are about warnings, you might also just accept the warning for now and not change your code. Or you apply the fix below and annotate it with a TODO: clean up syntax right away :)


The suggested solution is to provide no default value to the property and set its value in the initializer instead.

See in the Xcode release notes under Swift 5.6 -> Resolved Issues.

For your code the fix is like this:

@main
struct Pro_Music_2App: App {
    @StateObject var audioPlaybackManager: AudioPlaybackManager
    
    @StateObject var dataController = DataController()

    init() {
        self._audioPlaybackManager = StateObject(wrappedValue: AudioPlaybackManager())
    }    

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(audioPlaybackManager.manager)
                .environmentObject(audioPlaybackManager)
                .environmentObject(audioPlaybackManager.manager.playlistManager)
                .environmentObject(audioPlaybackManager.musicPlayerViewManager)
                .environmentObject(audioPlaybackManager.scrubberViewModel)
                .environment(\.managedObjectContext, dataController.container.viewContext)
                
        }
    }
}

@MainActor class AudioPlaybackManager: ObservableObject {
    ...
}

The underlying problem is that initializers are not async. Therefore, you cannot hop between different actors. And since the compiler synthesizes an initializer for default-values, this also affects default-values. That is, if e.g. your DataController was annotated with a different global actor than @MainActor, then the initializer had to hop between the @MainActor and whatever actor you're using on DataController. That, however, is impossible because init cannot be async.

I guess it would be difficult to implement this single-actor check for stored properties' default-values in the compiler, therefore they just decided to not allow actor protected default-values in this context. As a result, we have to write the initializer ourselves.

For reference see: https://github.com/apple/swift-evolution/blob/main/proposals/0327-actor-initializers.md#stored-property-isolation

theMomax
  • 919
  • 7
  • 8
  • What's weird is that they don't want you to use the `init(wrappedValue:)` either - https://developer.apple.com/documentation/swiftui/stateobject/init(wrappedvalue:). Seems like the only "correct" fix is to mark each method as MainActor? – atultw Mar 23 '22 at 18:26
  • 1
    Yes, using this syntax was discouraged previously. I feel like this is an intermediary solution and hope that this situation will be resolved with Swift 6. – theMomax Mar 24 '22 at 09:43
  • 1
    Apple's official sample code uses default value and @MainActor too. https://developer.apple.com/tutorials/sample-apps/memecreator – Benjamin Apr 23 '22 at 01:30
  • All of this is in a state [sic] of flux. The reference to `init(wrappedValue:)` in the doc is prior to all of this. The sample code mentioned (I think) came before 5.3. – Michael Long Apr 24 '22 at 18:06
  • 1
    And according to https://twitter.com/andresr_develop/status/1509287460961927186?s=21 things may change still. My preferred solution is not mark the entire class with @MainActor and instead use the attribute on any function called directly from a `.task { }` modifier. – Michael Long Apr 24 '22 at 18:09
3

I had the same issue and I have fixed it in this way:

@MainActor
class MultipleDownloadsViewModel: NSObject, ObservableObject {
    @Published var downloads: [DownloadModel]

    // Init of properties in actor: see https://stackoverflow.com/questions/71396296/how-do-i-fix-expression-requiring-global-actor-mainactor-cannot-appear-in-def/71412877#71412877
    @MainActor override init() {
        downloads = [
            DownloadModel(fileToDownload: "https://speed.hetzner.de/100MB.bin"),
            DownloadModel(fileToDownload: "https://speed.hetzner.de/1GB.bin"),
            DownloadModel(fileToDownload: "https://speed.hetzner.de/10GB.bin")
        ]
    }

    ...
}