Skip to content

Commit 4d74f7a

Browse files
authored
(151862798) Resolve AttributedString attribute storage Sendable warnings (#1313)
1 parent a456c0b commit 4d74f7a

File tree

3 files changed

+54
-4
lines changed

3 files changed

+54
-4
lines changed

Sources/FoundationEssentials/AttributedString/AttributedStringAttributeStorage.swift

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,21 @@ extension AttributedString {
3232
inheritedByAddedText = K.inheritedByAddedText
3333
invalidationConditions = K.invalidationConditions
3434
}
35+
36+
#if FOUNDATION_FRAMEWORK
37+
@inline(__always)
38+
private static func _unsafeAssumeSendableRawValue<T>(_ value: T) -> RawValue {
39+
// Perform this cast in a separate function unaware of the T: Hashable constraint to avoid compiler warnings when performing the Hashable --> Hashable & Sendable cast
40+
value as! RawValue
41+
}
42+
43+
fileprivate init<K: AttributedStringKey>(assumingSendable value: K.Value, for key: K.Type) {
44+
_rawValue = Self._unsafeAssumeSendableRawValue(value)
45+
runBoundaries = K.runBoundaries
46+
inheritedByAddedText = K.inheritedByAddedText
47+
invalidationConditions = K.invalidationConditions
48+
}
49+
#endif
3550

3651
var isInvalidatedOnTextChange: Bool {
3752
invalidationConditions?.contains(.textChanged) ?? false
@@ -61,6 +76,18 @@ extension AttributedString {
6176
return value
6277
}
6378

79+
#if FOUNDATION_FRAMEWORK
80+
fileprivate func rawValueAssumingSendable<K: AttributedStringKey>(
81+
as key: K.Type
82+
) -> K.Value {
83+
// Dynamic cast instead of an identity cast to support bridging between attribute value types like NSColor/UIColor
84+
guard let value = self._rawValue as? K.Value else {
85+
preconditionFailure("Unable to read \(K.self) attribute: stored value of type \(type(of: self._rawValue)) is not key's value type (\(K.Value.self))")
86+
}
87+
return value
88+
}
89+
#endif
90+
6491
static func ==(left: Self, right: Self) -> Bool {
6592
func openEquatableLHS<LeftValue: Hashable & Sendable>(_ leftValue: LeftValue) -> Bool {
6693
func openEquatableRHS<RightValue: Hashable & Sendable>(_ rightValue: RightValue) -> Bool {
@@ -193,6 +220,25 @@ extension AttributedString._AttributeStorage {
193220
get { self[T.name]?.rawValue(as: T.self) }
194221
set { self[T.name] = .wrapIfPresent(newValue, for: T.self) }
195222
}
223+
224+
#if FOUNDATION_FRAMEWORK
225+
/// Stores & retrieves an attribute value bypassing the T.Value : Sendable constraint
226+
///
227+
/// In general, callers should _always_ use the subscript that contains a T.Value : Sendable constraint
228+
/// This subscript should only be used in contexts when callers are forced to work around the lack of an AttributedStringKey.Value : Sendable constraint and assume the values are Sendable (ex. during NSAttributedString conversion while iterating scopes)
229+
subscript <T: AttributedStringKey>(assumingSendable attribute: T.Type) -> T.Value? {
230+
get {
231+
self[T.name]?.rawValueAssumingSendable(as: T.self)
232+
}
233+
set {
234+
guard let newValue else {
235+
self[T.name] = nil
236+
return
237+
}
238+
self[T.name] = _AttributeValue(assumingSendable: newValue, for: T.self)
239+
}
240+
}
241+
#endif
196242

197243
subscript (_ attributeName: String) -> _AttributeValue? {
198244
get { self.contents[attributeName] }

Sources/FoundationEssentials/AttributedString/AttributedStringCodable.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,8 @@ extension AttributedString : CodableWithConfiguration {
228228
{
229229
let attributeEncoder = attributesContainer.superEncoder(forKey: AttributeKey(stringValue: name)!)
230230
func project<K: EncodableAttributedStringKey>(_: K.Type) throws {
231-
try K.encode(attributes[K.self]!, to: attributeEncoder)
231+
// We must assume that the value is Sendable here because we are dynamically iterating a scope and the attribute keys do not statically declare the values are Sendable
232+
try K.encode(attributes[assumingSendable: K.self]!, to: attributeEncoder)
232233
}
233234
try project(encodableAttributeType)
234235
} // else: the attribute was not in the provided scope or was not encodable, so drop it
@@ -336,7 +337,8 @@ extension AttributedString : CodableWithConfiguration {
336337
let decodableAttributeType = attributeKeyType as? any DecodableAttributedStringKey.Type
337338
{
338339
func project<K: DecodableAttributedStringKey>(_: K.Type) throws {
339-
attributes[K.self] = try K.decode(from: try attributesContainer.superDecoder(forKey: key))
340+
// We must assume that the value is Sendable here because we are dynamically iterating a scope and the attribute keys do not statically declare the values are Sendable
341+
attributes[assumingSendable: K.self] = try K.decode(from: try attributesContainer.superDecoder(forKey: key))
340342
}
341343
try project(decodableAttributeType)
342344
}

Sources/FoundationEssentials/AttributedString/Conversion.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,8 @@ extension AttributeContainer {
111111
for (key, value) in dictionary {
112112
if let type = attributeTable[key.rawValue] {
113113
func project<K: AttributedStringKey>(_: K.Type) throws {
114-
storage[K.self] = try K._convertFromObjectiveCValue(value as AnyObject)
114+
// We must assume that the value is Sendable here because we are dynamically iterating a scope and the attribute keys do not statically declare the values are Sendable
115+
storage[assumingSendable: K.self] = try K._convertFromObjectiveCValue(value as AnyObject)
115116
}
116117
do {
117118
try project(type)
@@ -145,7 +146,8 @@ extension Dictionary where Key == NSAttributedString.Key, Value == Any {
145146
for key in container.storage.keys {
146147
if let type = attributeTable[key] {
147148
func project<K: AttributedStringKey>(_: K.Type) throws {
148-
self[NSAttributedString.Key(rawValue: key)] = try K._convertToObjectiveCValue(container.storage[K.self]!)
149+
// We must assume that the value is Sendable here because we are dynamically iterating a scope and the attribute keys do not statically declare the values are Sendable
150+
self[NSAttributedString.Key(rawValue: key)] = try K._convertToObjectiveCValue(container.storage[assumingSendable: K.self]!)
149151
}
150152
do {
151153
try project(type)

0 commit comments

Comments
 (0)