Monday, April 10, 2017

Changing the line spacing only for the last line in a label with attributed text

Lately I've been working on a code-only UICollectionView, and I've been experimenting a little bit with the self-sizing UICollectionViewCells. Needless to say, nightmares followed. Having been involved in Android development previously, it was never clear to me why UILabels behave the way they do – the default behavior being truncating – and not automatically create new lines without what's called Auto Layout.

I wanted to create a collection view, which would list the notifications in an app. I was after something that looked like below:

The notification text could be arbitrarily long, with the time indicator being a little bit lower at the bottom. Now, every single developer that has a little bit of experience with auto layout knows that working with multiple labels is a one-way ticket to programmer's hell. So using a separate label for the time was a no-no. Thankfully, there's also NSAttributedString. With this type of object, it is possible to create a string which is composed of multiple substrings, each with their own custom styling. So I could create the text above using something like below:

notificationAttrText = NSMutableAttributedString(string: notificationText, attributes: [NSFontAttributeName: UIFont.systemFont(ofSize: 14)])

timeAttrText = NSMutableAttributedString(string: "\n\(timeText)", attributes: [NSFontAttributeName: UIFont.systemFont(ofSize: 11), NSForegroundColorAttributeName: UIColor.init(colorLiteralRed: 158/255, green: 158/255, blue: 158/255, alpha: 1)])

let combined = NSMutableAttributedString()
notificationLabel.attributedText = combined

This was enough to create my label to look the way I wanted it, but... The time text was pretty close to the notification text.
At first I thought this is a pretty easy one. There is a way to add paragraph styling to an NSMutableAttributedString, by specifying the NSMutableParagraphStyle which you create, and also the range of the text you want to apply this attribute on. The drawback of this method is that you need to know the exact length of the line above. This is because you need two lines to define the spacing between them , so the range must be from the beginning of the line above, to the ending of the line below. In my case I needed to know the range for this piece of text "spacing only for the last line in a NSAttributedText\n2 hours ago". Obviously, it's not an easy thing to do. There's no easy way to get a specific line in a label. So I was doomed...

Chapter II - Redemption

Except I wasn't. NSMutableParagraphStyle to the rescue, everyone. But how, I hear you asking. Well, the answer lies on another property of this type of object. Not lineSpacing, but lineHeightMultiple. By using this property, I only needed to know the range for the time text, which I could very easily determine.

let paragraph = NSMutableParagraphStyle()
paragraph.lineHeightMultiple = 1.2
let length = _timeAttrText.length
let start = combined.length - length
combined.addAttribute(NSParagraphStyleAttributeName, value: paragraph, range: NSMakeRange(start, length))

The actual line height is multiplied by the value you assign to the lineHeightMultiple attribute, so a value of 1.2 would actually raise the height of the line by 20%, giving me the result I wanted.

No comments:

Post a Comment