Let's suppose we have this simple SwiftData model.
import Foundation
import SwiftData
@Model
final class Item {
var timestamp: Date
init(timestamp: Date) {
self.timestamp = timestamp
}
}
How to configure an EnvironmentKey
with the new @Entry
macro that uses the Item
type?
The code below triggers an error at build time:
Static property 'defaultValue' is not concurrency-safe because non-'Sendable' type 'EnvironmentValues.__Key_someItem.Value' (aka 'Optional') may have shared mutable state
import SwiftUI
import SwiftData
extension EnvironmentValues {
@Entry var someItem: Item? = nil
}
Tested with Xcode 16.0 beta (16A5171c) / Swift 6 / iOS 18.
As of the full release of Xcode 16.0, this is no longer an issue. @Entry
now generates a computed property for the default value.
@Entry
seems to be designed to generate something like this for the defaultValue
:
static let defaultValue: Item? = nil
This is not safe as per SE-0412.
If it had generated
static var defaultValue: Item? { nil }
There would have been no warnings. It's probably not designed to do that because nil
here will be evaluated each time defaultValue
is accessed. This goes against people's expectations of expressions after a =
, which is usually only evaluated once. An extreme example:
// user expects someSideEffect() to only be called once, but if this
// were put into a computed property, it could be run multiple times
@Entry var someItem: Item? = someSideEffect()
Here is a macro implementation that generates a computed defaultValue
instead.
// implementation:
enum EntryMacro: AccessorMacro, PeerMacro {
static func expansion(of node: AttributeSyntax, providingPeersOf declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext) throws -> [DeclSyntax] {
guard let binding = declaration.as(VariableDeclSyntax.self)?.bindings.first,
let type = binding.typeAnnotation?.type,
let defaultValueExpr = binding.initializer?.value,
let name = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier.text else {
return []
}
return [
"""
private struct \(raw: keyTypeName(forKeyName: name)): Key {
static var defaultValue: \(type) { \(defaultValueExpr) }
}
"""
]
}
static func expansion(of node: AttributeSyntax, providingAccessorsOf declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext) throws -> [AccessorDeclSyntax] {
guard let binding = declaration.as(VariableDeclSyntax.self)?.bindings.first,
let name = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier.text else {
return []
}
return [
"""
get { self[\(raw: keyTypeName(forKeyName: name)).self] }
""",
"""
set { self[\(raw: keyTypeName(forKeyName: name)).self] = newValue }
"""
]
}
static func keyTypeName(forKeyName name: String) -> String {
"__Key_\(name)"
}
}
// declaration:
@attached(accessor, names: named(get), named(set))
@attached(peer, names: prefixed(__Key_))
public macro ComputedDefaultEntry() = #externalMacro(module: "...", type: "EntryMacro")
// additional things that I will explain below...
public extension EnvironmentValues {
typealias Key = EnvironmentKey
}
public extension FocusedValues {
typealias Key = FocusedValueKey
}
public extension Transaction {
typealias Key = TransactionKey
}
public extension ContainerValues {
typealias Key = ContainerValueKey
}
SwiftUI's @Entry
macro works in many types - EnvironmentValues
, Transaction
, FocusedValues
, etc. I've also added the same functionality to this @ComputedDefaultEntry
macro. It generates a key type conforming to "Key
". I have declared Key
as type aliases in each of the applicable types, so that in EnvironmentValues
it will resolve to EnvironmentKey
, and in FocusedValues
it will resolve to FocusValueKey
, and so on.