How to address "static property 'foo' is not concurrency-safe" warning?

The GardenGreens project below gives a "static property 'samples' is not concurrency-safe" warning when Strict Concurrency Checking is set to Complete.

Is this a false positive or compiler bug? If not, what's the best way to address it?

Here's the full project code in case anyone wants to explore…

GardenGreensApp.swift

import SwiftUI

@main
struct GardenGreensApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }.modelContainer(for: [Vegetable.self])
    }
}

ContentView.swift

import SwiftUI
import SwiftData

// MARK: - Model
@Model
final class Vegetable: Identifiable {
    var name: String = ""

    init(name: String) {
        self.name = name
    }
}

// MARK: - View
struct ContentView: View {
    @Environment(\.modelContext) private var context
    @Query private var vegetables: [Vegetable]

    @State private var name: String = ""

    var body: some View {
        TextField("Vegetable name", text: $name)
            .onSubmit {
                let vegetable = Vegetable(name: name)
                context.insert(vegetable)
                name = ""
            }
            .padding()

        List(vegetables) { vegetable in
            Text(vegetable.name)
        }
    }
}

// MARK: - Preview Container
@MainActor
let previewContainer: ModelContainer = { 
    do {
        let container = try ModelContainer(for: Vegetable.self, configurations: ModelConfiguration(isStoredInMemoryOnly: true))

        SampleData.samples.forEach { vegetable in
            container.mainContext.insert(vegetable)
        }

        return container
    } catch {
        fatalError("Failed to create container.")
    }
}()

struct SampleData {
    static let samples: [Vegetable] = { // <- Static property 'samples' is not concurrency-safe because it is not either conforming to 'Sendable' or isolated to a global actor; this is an error in Swift 6
        return ["Tomato", "Turnip", "Onion", "Red Pepper"].map { Vegetable(name: $0) }
    }()
}

// MARK: - Preview
#Preview { @MainActor in
    ContentView()
        .modelContainer(previewContainer)
}

That is a correct warning. SwiftData models does not conform to Sendable, and one of the requirements of global variables (even non-mutable like in your case) is sendability. Your class type Vegetable could simply be mutated from different concurrency domains. You can handle that by isolation on a global actor, so access is only happens from one concurrency domain.

1 Like

Thanks for the confirmation! I only asked if it was a complier issue because the last time I got a similar warning, Holly Borla said it might be a compiler bug (see this Mastodon thread)

You can handle that by isolation on a global actor, so access is only happens from one concurrency domain.

I'm new to Swift/SwiftUI and haven't delved in concurrency much. Can you point me to an doc or tutorial on how to isolate a global actor?

BTW, I'm only using that pattern to populate my Xcode previews, so I'm not sure I should even be worried about it! :person_shrugging: I just can't stand having any warnings left after a build…OCD!? :laughing:

I suppose right now the only way to understand global actors is proposal itself - swift-evolution/proposals/0316-global-actors.md at main · apple/swift-evolution · GitHub. It might be a lot to understand, so I suggest first get with 2 things:

  1. Sendable Types to understand what's it all about.
  2. @MainActor is a global actor, and you already using it, which it (somewhat unfortunate) currently the most easy way to get sendability, especially in SwiftUI apps.

BTW, I'm only using that pattern to populate my Xcode previews, so I'm not sure I should even be worried about it! :person_shrugging: I just can't stand having any warnings left after a build…OCD!? :laughing:

I think that is a great habit to keep project clean from warnings, even such ones. This is one of my rules - there should be no warnings (ideally) at any given point of time. You can collect a lot of such "unimportant' warnings and eventually miss important-one.

1 Like

If this weren't a Swift Data model, then the easiest solution would probably be to just make Vegetable Sendable, either by making name immutable or just by making it a struct — presumably, you aren't worried about changing the name of a vegetable dynamically. But yeah, that approach doesn't work with Swift Data, and I assume your example is reduced from a more complex type that does require mutability.

2 Likes

Thanks, @vns, I'll take a look at those and see if I can get my head around it!

I did try making the class sendable, but didn't get very far as it gave me a different warning in the model's @_PersistedProperty macro expansion:

Stored property '_name' of 'Sendable'-conforming class 'Vegetable' is mutable

@John_McCall, you're correct that the Garden Greens example is a simplified version of what I'm doing in an app I'm building, but it came from this Udemy course.

If I want to keep the SwiftData model, what's the best approach to populating previews with sample data?

As @John_McCall mentioned above, marking Vegetable in this case wouldn't work with SwiftData. Models there are not sendable by design, they intentionally were made to be so. You can read about its, or CoreData design - which a bit less abstracted away in my taste, decisions in Apple documentation or articles on the topic.

I personally would made a solution like that in case with previews (only):

struct SampleData {
    static var samples: [Vegetable] {
        return ["Tomato", "Turnip", "Onion", "Red Pepper"].map { Vegetable(name: $0) }
    }
}

Now this is a computed property, so it safe to access from whatever you want since each time a new array will be created, and there is no need to isolated on @MainActor.

However, I want you to be highly accurate with this approach, because in case of simple previews it works fine - overhead of creating it each time isn't noticeable. For other cases outside of previews you can just isolate on @MainActor:

struct SampleData {
    @MainActor
    static let samples: [Vegetable] = {
        return ["Tomato", "Turnip", "Onion", "Red Pepper"].map { Vegetable(name: $0) }
    }()
}
2 Likes

Thank you, those examples help a lot; I'm a big fan of examples! And it's nice to have two options in my back pocket as I work on my app.

I appreciate you both chiming in! :pray:

1 Like