How to easily present strings with bold text and links in Cocoa
March 25, 2015
Have you ever needed to present something like this in your Cocoa UI?
Maybe in a popover?
The most native way to do this in Cocoa is with
NSAttributedString. You can set the attributed string source, and then add URL and different font ranges to the attributed string, and it all works just fine. Just about any UI component that presents strings these days can take an attributed string, and that certainly includes the multiline
NSTextField that’s being used here.
Pushing the requirements
Well, so far, so good. We can easily construct this in code. But let’s push on it a bit. How do you easily present strings with bold text and links while also making the content easily editable?
By “easily editable”, I mean just changing the string without having to change your source code.
This is what I needed to do recently. I needed to present exactly the style of strings that you see, containing bold text and clickable links. If the strings don’t change, I can construct this as a one-off in code, but how do you do it without having to edit your code every time you change the content?
Markdown is a fantastic markup format for this kind of thing. It may not be a formally defined standard, but it’s easy to read and write. So, the requirement I came up with is, I want to enter the source string as Markdown and get NSAttributedString as a result. To do this conversion, I needed some kind of parser.
There are many full-featured Markdown parsers available for Cocoa, but I was really short on time and I did not want to go through a full process of finding the best one and adding it to my project as a dependency. I needed to solve a focused problem of parsing Markdown-style “strong” text and links, and decided that a focused solution is best in this case. So, in the spirit of “Not-Invented-Here”, I decided to write my own. And here it is as a Gist.
There’s a few things about this solution. It’s not been fully battle-tested, there’s no formal conformance tests, and I wouldn’t trust it in a hostile environment. It assumes that you fully control the input and just need to present a string with some correctly formatted strong and link tags in it. I wouldn’t trust this solution at all with input from the user.
Also, it uses regular expressions and not a proper parser. You know what they say about regexes… “You have a problem. You decide to solve it with a regex. Now you have two problems.” This is one of the cases where my lack of Computer Science background is starting to show, in the form of lacking parser/lexer theory and practice. I really wish I had more background in this. (Any easy courses or materials you’d recommend?) Two input classes are about the maximum that this kind of solution can handle, and it already looks like spaghetti. If I had to do more kinds of input, I’d look at a real parser.
Rounding it up
There’s a few things you need to do to get from a Markdown-like string to the above example screenshots. You need to augment the resulting NSAttributedString with a paragraph style and provide the input fonts and colors. Here’s a full real example.
NSString *inputString = @"Oh look, I’m a string.\n\nI contain some **strong text** and even a [link](http://example.com) in several paragraphs.\n\nIn fact, **here’s another strong point** with a [link.](http://example.com)"; NSMutableAttributedString *attributed = [[inputString attributedStringFromLinksAndStrongTextWithBaseFont:[NSFont systemFontWithSize:13] strongFont:[NSFont boldSystemFontWithSize:13] urlColor:[NSColor blueColor]] mutableCopy]; NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init]; style.alignment = NSCenterTextAlignment; style.lineSpacing = 4; [attributed addAttribute:NSParagraphStyleAttributeName value:style range:NSMakeRange(0, attributed.string.length)]; self.textField.attributedStringValue = attributed;
There’s a few tools I used to put this together fairly quickly.
First, CodeRunner to build and test the code. This is my favorite app to work with small code snippets in languages like Objective-C or Swift. It’s silly how Swift promotes its playground idea, and if you want to actually test with a playground, the first thing you must do is to create a file, give it a name and pick a save location for your throwaway code. Coderunner has none of that silliness. It may not have the fancy live feedback of a playground, but it lets you test and run code snippets with much less overhead.
From the same author comes Patterns, a dedicated app for testing regular expressions.
Finally, if you get an attributed string using the above technique and actually present it in your UI in a NSTextField, you’ll find that the color behaves weirdly and reverts to the default blue. This may not be what you want. To help out, you can use NSTextFieldHyperlinks.