Xcode 15.3 Beta 2 shows Concurrency warning when using Main actor-isolated default values (in SwiftUI)

Hey, I have another question about the upgraded Concurrency warnings that came with the newest Xcode 15.3 Beta (with the Concurrency Checker set to "complete").

The concrete example is about SwiftUI, but the underlying problem is Concurrency:

I have a setup that makes use of the new @Observable macro to inject objects into the SwiftUI environment. This seems to be officially supported, if I understand the documentation correctly (Environment | Apple Developer Documentation, though it seems to be missing the creation of the keyPath for the environment value).

That has worked well so far, but with Xcode 15.3 Beta 2 I get tons of warnings with that. Here's a minimal example:

import SwiftUI

@MainActor @Observable
final class SomeDataProvider {
  init() {}
}

extension SomeDataProvider {
  static let live = SomeDataProvider()
}

extension EnvironmentValues {
  var someDataProvider: SomeDataProvider {
    get { self[SomeDataProviderKey.self] }
    set { self[SomeDataProviderKey.self] = newValue }
  }
}

private struct SomeDataProviderKey: EnvironmentKey {
  // Warning: Main actor-isolated default value in a nonisolated context; this is an error in Swift 6
  static let defaultValue: SomeDataProvider = .live 
}

The problem is that EnvironmentKey is not Main actor-isolated (probably for a good reason), but that means we cannot use it to inject objects into the SwiftUI environment, since those objects should all be Main actor-isolated because they interact with UI.

Is this something that Apple has to solve in SwiftUI (i.e. with a new way of adding such objects to the environment), or am I just handling this wrong, from a Concurrency perspective? I've tried around doing different things but nothing seems to get rid off these warnings.

I appreciate any help :slightly_smiling_face:

2 Likes

You do need to fix this in your code. If you need to call a function in a nonisolated context, e.g. by using it as the value of a nonisolated static variable, then the function itself needs to be nonisolated. In this case, it's an initializer. Marking both SomeDataProvider.init and static let live as nonisolated should resolve the warning:

@MainActor @Observable
final class SomeDataProvider {
  nonisolated init() {}
}

extension SomeDataProvider {
  nonisolated static let live = SomeDataProvider()
}

In many cases, initializers of global actor types do not actually need to access actor isolated state, so they can be safely marked as nonisolated. In Swift 5.10 under -strict-concurrency=complete, the compiler will infer nonisolated on the synthesized initializers of global actor isolated types when possible.

4 Likes

Thanks a lot! That makes sense

Hey, I'm having the same issue as you above in two use cases,

One of them needs a let so I can easily change the init to nonisolated like you've done as that value will never change on that actor

The second has variables (it's an @MainActor @Observable) that need to be set inside the init to their default values which is pulled from UserDefaults - this class is a Preferences object I want to pass around in my environment.

The issue with this is making it nonisolated means I can no longer set these properties inside the init as they need to be set in the main actor

I'm still learning the concurrency stuff here and wanting to get rid of my Swift 6 warning in the process - is there anything obvious I'm missing here?

It does feel to me like the Environment should all be MainActor safe (like @State is) so I'm a bit confused on what's going on here

Here's my code where I've dumbed down the Preferences type for example purposes

import Foundation
import SwiftUI

private struct PreferencesKey: EnvironmentKey {
    static let defaultValue = Preferences.shared // <- Main actor-isolated default value in a nonisolated context; this is an error in Swift 6
}

extension EnvironmentValues {
    var preferences: Preferences { self[PreferencesKey.self] }
}

@MainActor
@Observable
final class Preferences {
    static let shared = Preferences()
    
    var isNotificationsEnabled: Bool
    
    init() {
        self.isNotificationsEnabled = UserDefaults.standard.value(forKey: UserDefaultsKeys.isNotificationsEnabled) as? Bool ?? false
    }
}
2 Likes