How do I apply my custom attribute in an AttributedString in SwiftUI

Viewed 287

I have my custom attribute here, just for testing, the goal is to swap letters just to see how I can affect the string

enum SwapAttribute : AttributedStringKey {
    typealias Value = String
    static let name = "swap"
}

extension AttributeScopes {
    struct MyTextStyleAttributes: AttributeScope {
        let swap: SwapAttribute
    }
}

extension AttributeDynamicLookup {
    subscript<T: AttributedStringKey>(dynamicMember keyPath: KeyPath<AttributeScopes.MyTextStyleAttributes, T>) -> T {
        return self[T.self]
    }
}

func attributesTest() {
    var str = AttributedString("It's a secret")
    let secret = str.range(of: "secret")!
    str[secret].swap = "*"
    // expect 'It's a ******'
    print(str)
}

I can add my attribute, and I can see it in the .runs variable, but all of the documentation and tutorials gloss over this. How do I execute code or apply changes to the string.

(or perhaps this is intended for other things)

1 Answers

Custom Implementations

Custom attributes only serve as data storage, they are not displayed or processed by SwiftUI or UIKit. Additional custom behavior would have to be implemented manually.

Workaround

For behavior as described in the question, stick to extensions.

var str = AttributedString("It's a ******")
let secret = str.range(of: "******")!
str[secret].swap = "secret"

That would display "It's a ******", while still holding your "secret" data in the attributes.

To improve on that, you could create an extension:

extension AttributedString {
    func swapping(_ value: String, with new: Character) -> AttributedString {
        let mutableAttributedString = NSMutableAttributedString(self)
        let range = mutableAttributedString.mutableString.range(of: value)
        let newString = String(repeating: new, count: range.length)
        
        // Use replaceCharacters(in:with:) to keep all original attributes
        mutableAttributedString.replaceCharacters(in: range, with: newString)

        // Set the "secret" data
        var str = AttributedString(mutableAttributedString)
        str[str.range(of: newString)!].swap = value

        return str
    }
}

var str = AttributedString("It's a secret")
    .swapping("secret", with: "*")

print(str)

The print output:

It's a  {
}
****** {
    swap = secret
}
Related