I am using a instance of UIWebView to process some text and color it correctly, it gives the result as HTML but rather than displaying it in the UIWebView I want to display it using Core Text with a NSAttributedString.

I am able to create and draw the NSAttributedString but I am unsure how I can convert and map the HTML into the attributed string.

I understand that under Mac OS X NSAttributedString has a initWithHTML: method but this was a Mac only addition and is not available for iOS.

I also know that there is a similar question to this but it had no answers, I though I would try again and see whether anyone has created a way to do this and if so, if they could share it.


In iOS 7, UIKit added an initWithData:options:documentAttributes:error: method which can initialize an NSAttributedString using HTML, eg:

[[NSAttributedString alloc] initWithData:[htmlString dataUsingEncoding:NSUTF8StringEncoding] 
                                 options:@{NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType,
                                           NSCharacterEncodingDocumentAttribute: @(NSUTF8StringEncoding)} 
                      documentAttributes:nil error:nil];

In Swift:

let htmlData = NSString(string: details).data(using: String.Encoding.unicode.rawValue)
let options = [NSAttributedString.DocumentReadingOptionKey.documentType:
        NSAttributedString.DocumentType.html]
let attributedString = try? NSMutableAttributedString(data: htmlData ?? Data(),
                                                          options: options,
                                                          documentAttributes: nil)

There is a work-in-progress open source addition to NSAttributedString by Oliver Drobnik at Github. It uses NSScanner for HTML parsing.


Creating an NSAttributedString from HTML must be done on the main thread!

Update: It turns out that NSAttributedString HTML rendering depends on WebKit under the hood, and must be run on the main thread or it will occasionally crash the app with a SIGTRAP.

New Relic crash log:

enter image description here

Below is an updated thread-safe Swift 2 String extension:

extension String {
    func attributedStringFromHTML(completionBlock:NSAttributedString? ->()) {
        guard let data = dataUsingEncoding(NSUTF8StringEncoding) else {
            print("Unable to decode data from html string: \(self)")
            return completionBlock(nil)
        }

        let options = [NSDocumentTypeDocumentAttribute : NSHTMLTextDocumentType,
                   NSCharacterEncodingDocumentAttribute: NSNumber(unsignedInteger:NSUTF8StringEncoding)]

        dispatch_async(dispatch_get_main_queue()) {
            if let attributedString = try? NSAttributedString(data: data, options: options, documentAttributes: nil) {
                completionBlock(attributedString)
            } else {
                print("Unable to create attributed string from html string: \(self)")
                completionBlock(nil)
            }
        }
    }
}

Usage:

let html = "<center>Here is some <b>HTML</b></center>"
html.attributedStringFromHTML { attString in
    self.bodyLabel.attributedText = attString
}

Output:

enter image description here


Swift initializer extension on NSAttributedString

My inclination was to add this as an extension to NSAttributedString rather than String. I tried it as a static extension and an initializer. I prefer the initializer which is what I've included below.

Swift 4

internal convenience init?(html: String) {
    guard let data = html.data(using: String.Encoding.utf16, allowLossyConversion: false) else {
        return nil
    }

    guard let attributedString = try?  NSAttributedString(data: data, options: [.documentType: NSAttributedString.DocumentType.html, .characterEncoding: String.Encoding.utf8.rawValue], documentAttributes: nil) else {
        return nil
    }

    self.init(attributedString: attributedString)
}

Swift 3

extension NSAttributedString {

internal convenience init?(html: String) {
    guard let data = html.data(using: String.Encoding.utf16, allowLossyConversion: false) else {
        return nil
    }

    guard let attributedString = try? NSMutableAttributedString(data: data, options: [NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.html], documentAttributes: nil) else {
        return nil
    }

    self.init(attributedString: attributedString)
}
}

Example

let html = "<b>Hello World!</b>"
let attributedString = NSAttributedString(html: html)

This is a String extension written in Swift to return a HTML string as NSAttributedString.

extension String {
    func htmlAttributedString() -> NSAttributedString? {
        guard let data = self.dataUsingEncoding(NSUTF16StringEncoding, allowLossyConversion: false) else { return nil }
        guard let html = try? NSMutableAttributedString(data: data, options: [NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType], documentAttributes: nil) else { return nil }
        return html
    }
}

To use,

label.attributedText = "<b>Hello</b> \u{2022} babe".htmlAttributedString()

In the above, I have purposely added a unicode \u2022 to show that it renders unicode correctly.

A trivial: The default encoding that NSAttributedString uses is NSUTF16StringEncoding (not UTF8!).