swift-concurrencyswift6mainactor

How to share global constant properties?


I'm currently migrating my codebase to Swift 6 and I have global properties like these that are used in multiple areas:

extension LocalizedStringKey {
    static let cancel = LocalizedStringKey("cancel")
    static let save = LocalizedStringKey("save")
    static let apply = LocalizedStringKey("apply")
    static let reportBug = LocalizedStringKey("error_reportBug_subtitle")
...
}

These generate warnings when Swift Concurrency Checking is set to Complete:

Static property 'cancel' is not concurrency-safe because non-'Sendable' type 'LocalizedStringKey' may have shared mutable state; this is an error in the Swift 6 language mode

This extension is just for constant global variables. I have other extensions similar to this, e.g. CGFloat constant properties:

extension CGFloat {
    static let contentPadding: CGFloat = isPad ? 32 : 16
    static let cornerPadding: CGFloat = isPad ? 24 : 12
    static let verticalPadding: CGFloat = isPad ? 14 : 8
    static let horizontalPadding: CGFloat = isPad ? 24 : 12
...
}

They're not even mutable. Am I missing something?

Since these are used in the UI, I figured, I could mark them as @MainActor, which according to the documentation is A singleton actor whose executor is equivalent to the main dispatch queue.

I have different error types to manage errors shown to the users. Here's the protocol they conform to:

protocol ErrorProtocol: Error {
    var source: ErrorSource { get }
    var errorCode: String { get }
    var imageSystemName: String { get }
    var title: LocalizedStringKey { get }
    var subtitle: LocalizedStringKey? { get }
}

I have the following class that handles an array of toast models, which contains the data for the toast view. I've only included the function handling errors, but it can also show other types of messages, e.g. info, warnings, etc.

@Observable
@MainActor
class ToastManager {
    static let shared = ToastManager()
    private(set) var toasts: [ToastModel] = []
...
    func present(_ error: ErrorProtocol) {
        let toast = ToastModel(
            type: .error,
            title: error.title,
            subtitle: error.subtitle,
            imageSystemName: error.imageSystemName,
            errorCode: error.errorCode,
            toastTime: .long
        )
        withAnimation(.snappy) {
            toasts.append(toast)
        }
    }

I mainly access the ToastManager.shared.present(...) from a SwiftUI view's ViewModel. Here's a sample call:

extension MainView {
    @Observable
    final class ViewModel {
        ...
        func fetchData() {
            isLoading = true

            Task { // <--- This generates a warning: Passing closure as a 'sending' parameter risks causing data races between code in the current task and concurrent execution of the closure; this is an error in the Swift 6 language mode
                do {
                    try await fetchUserData()
                    isLoading = false
                    didFinishFolderSetup = true

                } catch {
                    isLoading = false
                    await ToastManager.shared.present(error) // This now needs `await`.
                }
            }
        }
    }
}

QUESTION: How can I handle the concurrency from the call site? Is my use of @MainActor even correct?

I'm quite unsure of how to properly "hand over" properties/results from one class to another, specifically if between calls, it switches between the main thread and the background thread, e.g.

func someMethod() {
    ...
    Task {
        // let result = await doSomeNetworkCall()
        // do something with the result
        ...
        // Update UI
    }
}

If someone can explain how this should be handled safely, that would be great. Thanks in advance!


Solution

    1. Because LocalizedStringKey is not conform Sendable so it can not safely pass to concurrency task. A workaround is provide @unchecked Sendable

      extension LocalizedStringKey: @unchecked Sendable {}

    2. For the error This generates a warning: Passing closure as a 'sending' parameter risks .... Firstly, it is not clear error show by compiler. Actually, it try telling you that access to isLoading, didFinishFolderSetup, fetchUserData can introduce data race. Try to imagine 2 fetchData get call immedietly after each other. So their might be 2 write access to isLoading in the same time. To fix error, a bold solution is mark ViewModel with @MainActor

    3. For the last question, using @MainActor for function interact with UI is correct. Another solution is MainActor.run