iosswiftnsattributedstringnsmutablestring

Keep text and image together in AttributedString


I have an attributed string in Swift which displays an icon next to a username. This works great, my implementation looks like this:

attributedUsername = NSMutableAttributedString(string: "username")
let iconAttachment = NSTextAttachment()
let iconImage = UIImage(named: "userIcon")
iconAttachment.image = iconImage
iconAttachment.bounds = CGRect(x: 0, y: -3, width: 14, height: 14)
let iconString = NSAttributedString(attachment: verifiedAttachment)
attributedUsername.append(iconString)

usernameLabel.attributedText = attributedUsername

However, sometimes a username is too large to fit on one line, wrapping the username on a second line (numberOfLines = 0). This is OK, but if the username is just long enough to fit on-screen, then the image wraps to the next line. I am wondering if there is any way to keep the icon wrapped to the end of the username. What I am looking to achieve, where * is the icon, is:

username *

longer username *

a very long
username *

instead of:

username *

longer username *

a very long username
*

So basically I want the icon to stick together with the last part of the username (if possible). If the username doesn't contain spaces and is too long, then it should just be wrapped on the next line because that would be standard implementation. Any suggestions?


Solution

  • Well, I am not sure if you can do this by setting some option in NSAttributedString, but you can easy achieve that with a simple algorithm.

    First, move the code that creates the attributed string to a function, since we'll be using it to calculate the width. Make sure to also set the font attribute, so it's possible to get the correct size out of the attributed string:

    func attributedString(for text: String) -> NSAttributedString {
        let attributedText = NSMutableAttributedString(string: text)
        let iconAttachment = NSTextAttachment()
        let iconImage = UIImage(named: "star")
        iconAttachment.image = iconImage
        iconAttachment.bounds = CGRect(x: 0, y: -3, width: 14, height: 14)
        let iconString = NSAttributedString(attachment: iconAttachment)
        attributedText.append(iconString)
        attributedText.setAttributes([.font: UIFont(name: "Avenir-Book", size: 15)!],
                                     range: NSRange((text.startIndex..<text.endIndex), in: text))
        return attributedText
    }
    

    Then:

    let text = "some really really really really long usernameeeeeeeee"
    let attributedText = attributedString(for: text)
    let maxWidth = ... 
    
    if attributedText.size().width > maxWidth { // A line break is required
        let lastWord = text.components(separatedBy: " ").last!
        let attributedLastWord = attributedString(for: lastWord)
        if attributedLastWord.size().width < maxWidth { // Forcing image to stick to last word
            var fixedText = text
            fixedText.insert("\n", at: text.index(text.endIndex, offsetBy: -lastWord.count))
            label.attributedText = attributedString(for: fixedText)
        } else {
            label.attributedText = attributedText
        }
    } else {
        label.attributedText = attributedText
    }
    

    Of course you will want to remove the force unwrap and other not so good practices. Those are just for brevity, though. I hope you got the idea.