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!
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 {}
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
For the last question, using @MainActor
for function interact with UI is correct. Another solution is MainActor.run