Search code examples
swiftmarkdownnsrange

Adding attribute to markdown NSAttributedString goes out of bounds


I have user input which can contain markdown or not. I want to show the user input on screen with rendered markdown. For some reason, I want to add my own static string which does not contain markdown to the end of the user input in the blue color.

It worked when I didn't used markdown. My guess is that Data is changing the original range, which makes my code invalid.

This is my code:

let trailing = "I MUST be blue :)"
let newString = "some *user* input" + trailing
let data = newString.data(using: .utf8)!
let replacedMessageText = try! NSMutableAttributedString(markdown: data)

replacedMessageText.addAttribute(
    NSAttributedString.Key.foregroundColor,
    value: UIColor.blue,
    // Here I want to find the range of my trailing static text
    range: (newString as NSString)
        // This line is probably wrong...
        .range(of: trailing, options: .backwards)

)

This crashes with this error:

"NSMutableRLEArray objectAtIndex:effectiveRange:: Out of bounds"

I did check out the methods on Data, but none of them returns an NSRange. How can I make my code work? Markdown does not support colored text :(.


Solution

  • For clarity of the explanation, let's do a little change:

    let trailing = "I MUST be blue :)"
    let newString = "some *user* input" + trailing
    

    ->

    let trailing = "I MUST be blue :)"
    let leading = "some *user* input"
    let newString = leading + trailing
    

    The issue is that newString is different from replacedMessageText.string, because the Markdown tags (here it's *) have been interpreted and removed. You can see it by printing replacedMessageText.string. So in your case, the range of the trailing is wrong (it's length is too big), hence the error. If you had reversed trailing and leading, it would have worked with your sample case, but you would have seen part of leading colored.

    Instead, you can do:

    let final = NSMutableAttributedString()
    let leadingAttrStr = try NSAttributedString(markdown: Data(leading.utf8))
    let trailingAttrStr = NSAttributedString(string: trailing, attributes: yourBlueAttributes)
    final.append(leadingAttrStr)
    final.append(trailingAttrStr)
    

    As a note (it's not your case, but if someone encounter the issue), if you have a Markdown tag that starts on leading and ends on trailing, it won't be rendered.

    Unrelated: newString.data(using: .utf8)! can be done with Data(newString.utf8), no need to force unwrap.