Convert HTML to NSAttributedString in iOS

IphoneObjective CCocoa TouchCore TextNsattributedstring

Iphone Problem Overview


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.

Iphone Solutions


Solution 1 - Iphone

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)

Solution 2 - Iphone

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

Solution 3 - Iphone

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

Solution 4 - Iphone

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)

Solution 5 - Iphone

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!).

Solution 6 - Iphone

Made some modification on Andrew's solution and update the code to Swift 3:

This code now use UITextView as self and able to inherit its original font, font size and text color

Note: toHexString() is extension from here

extension UITextView {
	func setAttributedStringFromHTML(_ htmlCode: String, completionBlock: @escaping (NSAttributedString?) ->()) {
		let inputText = "\(htmlCode)<style>body { font-family: '\((self.font?.fontName)!)'; font-size:\((self.font?.pointSize)!)px; color: \((self.textColor)!.toHexString()); }</style>"

		guard let data = inputText.data(using: String.Encoding.utf16) else {
			print("Unable to decode data from html string: \(self)")
			return completionBlock(nil)
		}

		DispatchQueue.main.async {
			if let attributedString = try? NSAttributedString(data: data, options: [NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType], documentAttributes: nil) {
				self.attributedText = attributedString
				completionBlock(attributedString)
			} else {
				print("Unable to create attributed string from html string: \(self)")
				completionBlock(nil)
			}
		}
	}
}

Example usage:

mainTextView.setAttributedStringFromHTML("<i>Hello world!</i>") { _ in }

Solution 7 - Iphone

Swift 3.0 Xcode 8 Version

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

Solution 8 - Iphone

Swift 4


  • NSAttributedString convenience initializer
  • Without extra guards
  • throws error

extension NSAttributedString {

    convenience init(htmlString html: String) throws {
        try self.init(data: Data(html.utf8), options: [
            .documentType: NSAttributedString.DocumentType.html,
            .characterEncoding: String.Encoding.utf8.rawValue
        ], documentAttributes: nil)
    }
    
}

Usage

UILabel.attributedText = try? NSAttributedString(htmlString: "<strong>Hello</strong> World!")

Solution 9 - Iphone

The only solution you have right now is to parse the HTML, build up some nodes with given point/font/etc attributes, then combine them together into an NSAttributedString. It's a lot of work, but if done correctly, can be reusable in the future.

Solution 10 - Iphone

Using of NSHTMLTextDocumentType is slow and it is hard to control styles. I suggest you to try my library which is called Atributika. It has its own very fast HTML parser. Also you can have any tag names and define any style for them.

Example:

let str = "<strong>Hello</strong> World!".style(tags:
    Style("strong").font(.boldSystemFont(ofSize: 15))).attributedString

label.attributedText = str

You can find it here https://github.com/psharanda/Atributika

Solution 11 - Iphone

The above solution is correct.

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

But the app wioll crash if you are running it on ios 8.1,2 or 3.

To avoid the crash what you can do is : run this in a queue. So that it always be on main thread.

Solution 12 - Iphone

Swift 3:
Try this:

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

And for using:

let str = "<h1>Hello bro</h1><h2>Come On</h2><h3>Go sis</h3><ul><li>ME 1</li><li>ME 2</li></ul> <p>It is me bro , remember please</p>"
        
self.contentLabel.attributedText = str.htmlAttributedString()

Solution 13 - Iphone

The built in conversion always sets the text color to UIColor.black, even if you pass an attributes dictionary with .forgroundColor set to something else. To support DARK mode on iOS 13, try this version of the extension on NSAttributedString.

extension NSAttributedString {
    internal convenience init?(html: String)                    {
        guard 
            let data = html.data(using: String.Encoding.utf16, allowLossyConversion: false) else { return nil }
        
        let options : [DocumentReadingOptionKey : Any] = [
            .documentType: NSAttributedString.DocumentType.html,
            .characterEncoding: String.Encoding.utf8.rawValue
        ]
        
        guard
            let string = try? NSMutableAttributedString(data: data, options: options,
                                                 documentAttributes: nil) else { return nil }

        if #available(iOS 13, *) {
            let colour = [NSAttributedString.Key.foregroundColor: UIColor.label]
            string.addAttributes(colour, range: NSRange(location: 0, length: string.length))
        }

        self.init(attributedString: string)
    }
}

Solution 14 - Iphone

Here is the Swift 5 version of Mobile Dan's answer:

public extension NSAttributedString {
    convenience init?(_ html: String) {
        guard let data = html.data(using: .unicode) else {
                return nil
        }

        try? self.init(data: data, options: [.documentType: NSAttributedString.DocumentType.html], documentAttributes: nil)
    }
}

Solution 15 - Iphone

Helpful Extensions

Inspired by this thread, a pod, and Erica Sadun's ObjC example in iOS Gourmet Cookbook p.80, I wrote an extension on String and on NSAttributedString to go back and forth between HTML plain-strings and NSAttributedStrings and vice versa -- on GitHub here, which I have found helpful.

The signatures are (again, full code in a Gist, link above):

extension NSAttributedString {
    func encodedString(ext: DocEXT) -> String?
    static func fromEncodedString(_ eString: String, ext: DocEXT) -> NSAttributedString? 
    static func fromHTML(_ html: String) -> NSAttributedString? // same as above, where ext = .html
}

extension String {
    func attributedString(ext: DocEXT) -> NSAttributedString?
}

enum DocEXT: String { case rtfd, rtf, htm, html, txt }

Solution 16 - Iphone

honoring font family, dynamic font I've concocted this abomination:

extension NSAttributedString
{
    convenience fileprivate init?(html: String, font: UIFont? = Font.dynamic(style: .subheadline))
    {
        guard let data = html.data(using: String.Encoding.utf8, allowLossyConversion: true) else {
        var totalString = html
        /*
         https://stackoverflow.com/questions/32660748/how-to-use-apples-new-san-francisco-font-on-a-webpage
            .AppleSystemUIFont I get in font.familyName does not work
         while -apple-system does:
         */
        var ffamily = "-apple-system"
        if let font = font {
            let lLDBsucks = font.familyName
            if !lLDBsucks.hasPrefix(".appleSystem") {
                ffamily = font.familyName
            }
            totalString = "<style>\nhtml * {font-family: \(ffamily) !important;}\n            </style>\n" + html
        }
        guard let data = totalString.data(using: String.Encoding.utf8, allowLossyConversion: true) else {
            return nil
        }
        assert(Thread.isMainThread)
        guard let attributedText = try?  NSAttributedString(data: data, options: [.documentType: NSAttributedString.DocumentType.html, .characterEncoding: String.Encoding.utf8.rawValue], documentAttributes: nil) else {
            return nil
        }
        let mutable = NSMutableAttributedString(attributedString: attributedText)
        if let font = font {
        do {
            var found = false
            mutable.beginEditing()
            mutable.enumerateAttribute(NSAttributedString.Key.font, in: NSMakeRange(0, attributedText.length), options: NSAttributedString.EnumerationOptions(rawValue: 0)) { (value, range, stop) in
                    if let oldFont = value as? UIFont {
                        let newsize = oldFont.pointSize * 15 * Font.scaleHeruistic / 12
                        let newFont = oldFont.withSize(newsize)
                        mutable.addAttribute(NSAttributedString.Key.font, value: newFont, range: range)
                        found = true
                    }
                }
                if !found {
                    // No font was found - do something else?
                }

            mutable.endEditing()
            
//            mutable.addAttribute(.font, value: font, range: NSRange(location: 0, length: mutable.length))
        }
        self.init(attributedString: mutable)
    }

}

alternatively you can use the versions this was derived from and set font on UILabel after setting attributedString

this will clobber the size and boldness encapsulated in the attributestring though

kudos for reading through all the answers up to here. You are a very patient man woman or child.

Solution 17 - Iphone

A function to convert html to Attributed NSAttributedString that will adapt dynamic size + adapt accessibility for the text.

static func convertHtml(string: String?) -> NSAttributedString? {
    
    guard let string = string else {return nil}
    
    guard let data = string.data(using: .utf8) else {
        return nil
    }
    
    do {
        let attrStr = try NSAttributedString(data: data,
                                      options: [.documentType: NSAttributedString.DocumentType.html, .characterEncoding: String.Encoding.utf8.rawValue],
                                      documentAttributes: nil)
        let range = NSRange(location: 0, length: attrStr.length)
        let str = NSMutableAttributedString(attributedString: attrStr)
        
        str.enumerateAttribute(NSAttributedString.Key.font, in: NSMakeRange(0, str.length), options: .longestEffectiveRangeNotRequired) {
            (value, range, stop) in
            if let font = value as? UIFont {
                
                let userFont =  UIFontDescriptor.preferredFontDescriptor(withTextStyle: .title2)
                let pointSize = userFont.withSize(font.pointSize)
                let customFont = UIFont.systemFont(ofSize: pointSize.pointSize)
                let dynamicText = UIFontMetrics.default.scaledFont(for: customFont)
                str.addAttribute(NSAttributedString.Key.font,
                                         value: dynamicText,
                                         range: range)
            }
        }

        str.addAttribute(NSAttributedString.Key.underlineStyle, value: 0, range: range)
        
        return NSAttributedString(attributedString: str.attributedSubstring(from: range))
    } catch {}
    return nil
    
}

To use:

let htmToStringText = convertHtml(string: html)
            
  self.bodyTextView.isEditable = false
  self.bodyTextView.isAccessibilityElement = true
  self.bodyTextView.adjustsFontForContentSizeCategory = true
  self.bodyTextView.attributedText = htmToStringText
  self.bodyTextView.accessibilityAttributedLabel = htmToStringText

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionJoshuaView Question on Stackoverflow
Solution 1 - IphonepixView Answer on Stackoverflow
Solution 2 - IphoneIngveView Answer on Stackoverflow
Solution 3 - IphoneAndrew SchreiberView Answer on Stackoverflow
Solution 4 - IphoneMobile DanView Answer on Stackoverflow
Solution 5 - IphonesamwizeView Answer on Stackoverflow
Solution 6 - IphoneHe Yifei 何一非View Answer on Stackoverflow
Solution 7 - IphonefssilvaView Answer on Stackoverflow
Solution 8 - IphoneAamirRView Answer on Stackoverflow
Solution 9 - IphonejerView Answer on Stackoverflow
Solution 10 - IphonePavel SharandaView Answer on Stackoverflow
Solution 11 - IphoneNitesh Kumar SinghView Answer on Stackoverflow
Solution 12 - Iphonereza_khalafiView Answer on Stackoverflow
Solution 13 - IphoneStephen OrrView Answer on Stackoverflow
Solution 14 - IphoneS1LENT WARRIORView Answer on Stackoverflow
Solution 15 - IphoneAmitaiBView Answer on Stackoverflow
Solution 16 - IphoneAnton TropashkoView Answer on Stackoverflow
Solution 17 - IphoneIshaView Answer on Stackoverflow