Adding custom attribute to NSAttributedString on iOS

NSAttributedString is used for displaying rich text on Apple platforms. There are many attributes available for styling the text. Let’s see how to add new attribute and how to draw it in UITextView.

Drawing custom attribute

Defining a new attribute is just a matter of extending NSAttributedString.Key and defining a new static variable. Difficult part is actually drawing the custom attribute. Apple’s documentation says that best way for it is to subclass NSLayoutManager and overriding drawGlyphs(forGlyphRange:at:). This is what we are doing here as well. Whenever glyphs are drawn, we check if the glyph range contains the custom attribute, if it does, then we get the rects for glyphs containing the custom attribute. When we know rects, we can draw the custom styling. In this case, we’ll mimic tokens in text and therefore go for rectangle with rounded corners. Should be noted that enumerating custom attribute, longestEffectiveRangeNotRequired should be specified. Otherwise, if we have two different tokens next to each-other, then this attribute is considered as one.

UITextView has by default text container inset set. For being able to draw the custom styling exactly behind the text, this offset must be taken account. Easiest is to propagate insets to layout manager what we are doing here.

extension NSAttributedString.Key {
    static let token = NSAttributedString.Key("Token")

final class TokenLayoutManager: NSLayoutManager {
    var textContainerOriginOffset: CGSize = .zero
    override func drawGlyphs(forGlyphRange glyphsToShow: NSRange, at origin: CGPoint) {
        let characterRange = self.characterRange(forGlyphRange: glyphsToShow, actualGlyphRange: nil)
        textStorage?.enumerateAttribute(.token, in: characterRange, options: .longestEffectiveRangeNotRequired, using: { (value, subrange, _) in
            guard let token = value as? String, !token.isEmpty else { return }
            let tokenGlypeRange = glyphRange(forCharacterRange: subrange, actualCharacterRange: nil)
            drawToken(forGlyphRange: tokenGlypeRange)
        super.drawGlyphs(forGlyphRange: glyphsToShow, at: origin)
    private func drawToken(forGlyphRange tokenGlypeRange: NSRange) {
        guard let textContainer = textContainer(forGlyphAt: tokenGlypeRange.location, effectiveRange: nil) else { return }
        let withinRange = NSRange(location: NSNotFound, length: 0)
        enumerateEnclosingRects(forGlyphRange: tokenGlypeRange, withinSelectedGlyphRange: withinRange, in: textContainer) { (rect, _) in
            let tokenRect = rect.offsetBy(dx: self.textContainerOriginOffset.width, dy: self.textContainerOriginOffset.height)
            UIColor(hue: 175.0/360.0, saturation: 0.24, brightness: 0.88, alpha: 1).setFill()
            UIBezierPath(roundedRect: tokenRect, cornerRadius: 4).fill()

Using custom layout manager in UITextView requires a little bit of setup what we can put into UITextView’s subclass.

final class TokenTextView: UITextView {
    init(frame: CGRect) {
        let layoutManager = TokenLayoutManager()
        let textStorage = NSTextStorage()
        let textContainer = NSTextContainer()
        textContainer.heightTracksTextView = true
        textContainer.widthTracksTextView = true
        super.init(frame: frame, textContainer: textContainer)
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    override var textContainerInset: UIEdgeInsets {
        didSet {
    private func updateLayoutManager() {
        guard let layoutManager = layoutManager as? TokenLayoutManager else { return }
        layoutManager.textContainerOriginOffset = CGSize(width: textContainerInset.left, height:
        layoutManager.invalidateDisplay(forCharacterRange: NSRange(location: 0, length: attributedText.length))
let textView = TokenTextView(frame: .zero)
textView.translatesAutoresizingMaskIntoConstraints = false
    textView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 16),
    textView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -16),
    textView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16),
    textView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -16)
let string = "The quick brown fox jumps over the lazy dog"
let attributedString = NSMutableAttributedString(string: string)
let value = "value"
attributedString.addAttribute(.token, value: value, range: NSRange(location: 4, length: 5))
textView.attributedText = attributedString


We defined a new attribute by extending NSAttributedString.Key. Then we created a new NSLayoutManager subclass and added custom styling to the attribute. Lastly, we configured the UITextView to use custom layout manager and set text with custom attribute to it.

If this was helpful, please let me know on Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Example project

NSAttributedStringCustomAttribute Xcode 11.2, Swift 5.1


Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s