Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,21 @@ extension AttributedString {
inheritedByAddedText = K.inheritedByAddedText
invalidationConditions = K.invalidationConditions
}

#if FOUNDATION_FRAMEWORK
@inline(__always)
private static func _unsafeAssumeSendableRawValue<T>(_ value: T) -> RawValue {
// Perform this cast in a separate function unaware of the T: Hashable constraint to avoid compiler warnings when performing the Hashable --> Hashable & Sendable cast
value as! RawValue
}

fileprivate init<K: AttributedStringKey>(assumingSendable value: K.Value, for key: K.Type) {
_rawValue = Self._unsafeAssumeSendableRawValue(value)
runBoundaries = K.runBoundaries
inheritedByAddedText = K.inheritedByAddedText
invalidationConditions = K.invalidationConditions
}
#endif

var isInvalidatedOnTextChange: Bool {
invalidationConditions?.contains(.textChanged) ?? false
Expand Down Expand Up @@ -61,6 +76,18 @@ extension AttributedString {
return value
}

#if FOUNDATION_FRAMEWORK
fileprivate func rawValueAssumingSendable<K: AttributedStringKey>(
as key: K.Type
) -> K.Value {
// Dynamic cast instead of an identity cast to support bridging between attribute value types like NSColor/UIColor
guard let value = self._rawValue as? K.Value else {
preconditionFailure("Unable to read \(K.self) attribute: stored value of type \(type(of: self._rawValue)) is not key's value type (\(K.Value.self))")
}
return value
}
#endif

static func ==(left: Self, right: Self) -> Bool {
func openEquatableLHS<LeftValue: Hashable & Sendable>(_ leftValue: LeftValue) -> Bool {
func openEquatableRHS<RightValue: Hashable & Sendable>(_ rightValue: RightValue) -> Bool {
Expand Down Expand Up @@ -193,6 +220,25 @@ extension AttributedString._AttributeStorage {
get { self[T.name]?.rawValue(as: T.self) }
set { self[T.name] = .wrapIfPresent(newValue, for: T.self) }
}

#if FOUNDATION_FRAMEWORK
/// Stores & retrieves an attribute value bypassing the T.Value : Sendable constraint
///
/// In general, callers should _always_ use the subscript that contains a T.Value : Sendable constraint
/// 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)
subscript <T: AttributedStringKey>(assumingSendable attribute: T.Type) -> T.Value? {
get {
self[T.name]?.rawValueAssumingSendable(as: T.self)
}
set {
guard let newValue else {
self[T.name] = nil
return
}
self[T.name] = _AttributeValue(assumingSendable: newValue, for: T.self)
}
}
#endif

subscript (_ attributeName: String) -> _AttributeValue? {
get { self.contents[attributeName] }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,8 @@ extension AttributedString : CodableWithConfiguration {
{
let attributeEncoder = attributesContainer.superEncoder(forKey: AttributeKey(stringValue: name)!)
func project<K: EncodableAttributedStringKey>(_: K.Type) throws {
try K.encode(attributes[K.self]!, to: attributeEncoder)
// 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
try K.encode(attributes[assumingSendable: K.self]!, to: attributeEncoder)
}
try project(encodableAttributeType)
} // else: the attribute was not in the provided scope or was not encodable, so drop it
Expand Down Expand Up @@ -336,7 +337,8 @@ extension AttributedString : CodableWithConfiguration {
let decodableAttributeType = attributeKeyType as? any DecodableAttributedStringKey.Type
{
func project<K: DecodableAttributedStringKey>(_: K.Type) throws {
attributes[K.self] = try K.decode(from: try attributesContainer.superDecoder(forKey: key))
// 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
attributes[assumingSendable: K.self] = try K.decode(from: try attributesContainer.superDecoder(forKey: key))
}
try project(decodableAttributeType)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ extension AttributeContainer {
for (key, value) in dictionary {
if let type = attributeTable[key.rawValue] {
func project<K: AttributedStringKey>(_: K.Type) throws {
storage[K.self] = try K._convertFromObjectiveCValue(value as AnyObject)
// 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
storage[assumingSendable: K.self] = try K._convertFromObjectiveCValue(value as AnyObject)
}
do {
try project(type)
Expand Down Expand Up @@ -145,7 +146,8 @@ extension Dictionary where Key == NSAttributedString.Key, Value == Any {
for key in container.storage.keys {
if let type = attributeTable[key] {
func project<K: AttributedStringKey>(_: K.Type) throws {
self[NSAttributedString.Key(rawValue: key)] = try K._convertToObjectiveCValue(container.storage[K.self]!)
// 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
self[NSAttributedString.Key(rawValue: key)] = try K._convertToObjectiveCValue(container.storage[assumingSendable: K.self]!)
}
do {
try project(type)
Expand Down