Markdown in Swift
A thought process I had last week:
- Man, it would be great if I could write descriptions for Barback in Markdown.
- Wait, maybe I can. There are Markdown compilers for everything, right?
- Wait, what would it even compile to? HTML? An
NSAttributedString
? How would that work? - Blargh. Let’s just Google things and hope for the best.
But for you, reader dearest, I have compiled my solution. It is thoroughly unexciting and likely suboptimal. Let us press on!
1. First, you need a Markdown compiler.
I selected Markingbird, because I’m a sucker for using Swift drop-ins. But there are many other solutions. (Don’t roll your own unless you like agony or premature optimization.) Throw this in your project. Swift means auto-importing. You invoke it like this:
var markdownEngine = Markdown()
let outputHTML = markdownEngine.transform(markdownText)
It is important that Markdown isn’t declared via let
, as it is self-mutating. You could probably get away with having it as a singleton, but it’s not threadsafe and the setup time is negligible (~10ms).
2. Next, you throw the HTML into an NSAttributedString.
This is surprisingly easy:
let informationData = informationHTML.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)
let informationString = NSAttributedString(data: informationData!, options: [NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType], documentAttributes: nil, error: nil)
There’s some bad news here: currently, this is done via rendering the whole dang thing in a UIWebView, which is like super slow. I’m talking approx 400ms slow. This is depressing, but still acceptable for my use case so I’m not too bummed: that being said, if anyone has a better way of accomplishing this let me know!
3. Lastly, you throw the NSAttributedString into a relevant view.
This should be self-explanatory for anyone who has experience with attributed strings: you just set the attributedText
attribute:
attributedText = informationString
There are two things to note about this, though:
Setting attributedText overwrites the style information of the label.
There are probably more sophisticated ways to fix this issue, but the styling I wanted was pretty fixed across the views so instead of iterating over the string elements I did it by straight-up adding CSS to the HTML before rendering it:
var informationHTML += "<style type='text/css'>"
+ "p { text-align: center;"
+ " font-family: \(font.familyName);"
+ " font-size: \(font.pointSize)px;"
+ " color: \(normalColor); }"
+ "</style>"
UILabels don’t play nice with hyperlinks.
Again, something that might be obvious, but if you’re dealing with Markdown you probably want support for hyperlinks – even if just the default handling mechanism – and you’re going to need to make sure you’re using a UITextView
with some specific attributes set:
view.selectable = true
view.editable = false
There are a couple ways you could implement all of this stuff – again, I’m not experienced with Swift to proselytize a certain approach as the most Swift-y 1. But, for better or for worse, this is how I implemented it – as an observed attribute of an existant subclass of UIWebView
:
import Foundation
import UIKit
class DescriptionTextView : UITextView {
// Other boring/irrelevant stuff is in this class, I promise.
var markdownText: NSString? {
didSet {
// Hey, short-circuiting logic!
if markdownText == nil || markdownText == "" {
attributedText = NSAttributedString(string: "")
return
}
var markdownEngine = Markdown()
let normalColor = NSString(format:"%2X", Color.Light.rawValue)
let tintColor = NSString(format:"%2X", Color.Tint.rawValue)
var informationHTML = "<style type='text/css'>"
+ "p { text-align: center;"
+ " font-family: \(font.familyName);"
+ " font-size: \(font.pointSize)px;"
+ " color: \(normalColor); }"
+ "</style>"
informationHTML += markdownEngine.transform(markdownText!)
let informationData = informationHTML.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)
let informationString = NSAttributedString(data: informationData!, options: [NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType], documentAttributes: nil, error: nil)
attributedText = informationString
}
}
}
- Number one complaint of Swift’s name is that it’s an adjective. What’s the Swift equivalent of “Pythonic”? [return]