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()
        
combined.append(_nameAttrText)
combined.append(_verbAttrText)
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.

Wednesday, April 5, 2017

Fixing "AppName.app is damaged and can’t be opened" in macOS

Most of the suggestions (all, actually) that I've seen about fixing the "AppName.app is damaged and can't be opened" error that pops up in macOS when opening a downloaded app, go through the route of disabling GateKeeper through the following command:

sudo spctl --master-disable

The problem is that most of the times, you can fix this issue without doing dangerous things; disabling GateKeeper is never a solution, it's simply an ugly workaround.

Anyways, this issue can sometimes be fixed with another command. Normally OSX (macOS) keeps track of file metadata, such as downloaded from. The metadata can be accessed through the xattr command line program.

I ran xattr on my downloaded app:

Eltons-MacBook-Pro:Applications elton$ xattr AppName.app/
com.apple.quarantine

So I figured I could delete the attribute:

Eltons-MacBook-Pro:Applications elton$ xattr -d com.apple.quarantine AppName.app/

Next, I tried to open the app, and bam! The app loaded without a warning. So that's it!