39

I found this string extension somewhere on SO that allows me to turn html code into an attributed string:

func html2AttributedString() -> NSAttributedString {
    return try! NSAttributedString(data: self.data(using: String.Encoding.unicode, allowLossyConversion: true)!, options: [NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType], documentAttributes: nil)
}

It worked fine in Swift 3, but with Swift 4, Xcode complains:

Cannot convert value of type 'NSAttributedString.DocumentAttributeKey' to expected dictionary key type 'NSAttributedString.DocumentReadingOptionKey'

How do I fix this?

LinusGeffarth
  • 27,197
  • 29
  • 120
  • 174

8 Answers8

93

You need to pass one of the available NSAttributedString DocumentType options:


Hypertext Markup Language (HTML) document.

static let html: NSAttributedString.DocumentType

Plain text document.

static let plain: NSAttributedString.DocumentType

Rich text format document.

static let rtf: NSAttributedString.DocumentType

Rich text format with attachments document.

static let rtfd: NSAttributedString.DocumentType

In this case you will need to pass the first one (html) NSAttributedString.DocumentType.html

So the extension updated to Swift 4 should look like this:

extension NSAttributedString {
    convenience init(data: Data, documentType: DocumentType, encoding: String.Encoding = .utf8) throws {
        try self.init(data: data,
                      options: [.documentType: documentType,
                                .characterEncoding: encoding.rawValue],
                      documentAttributes: nil)
    }
    convenience init(html data: Data) throws {
        try self.init(data: data, documentType: .html)
    }
    convenience init(txt data: Data) throws {
        try self.init(data: data, documentType: .plain)
    }
    convenience init(rtf data: Data) throws {
        try self.init(data: data, documentType: .rtf)
    }
    convenience init(rtfd data: Data) throws {
        try self.init(data: data, documentType: .rtfd)
    }
}

extension StringProtocol {
    var data: Data { return Data(utf8) }
    var htmlToAttributedString: NSAttributedString? {
        do {
            return try .init(html: data)
        } catch {
            print("html error:", error)
            return nil
        }
    }
    var htmlDataToString: String? {
        return htmlToAttributedString?.string
    }
}

extension Data {
    var htmlToAttributedString: NSAttributedString? {
        do {
            return try .init(html: self)
        } catch {
            print("html error:", error)
            return nil
        }

    }
}

Playground Testing

let htmlString = "<style type=\"text/css\">#red{color:#F00}#green{color:#0F0}#blue{color: #00F; font-weight: Bold; font-size: 32}</style><span id=\"red\" >Red</span><span id=\"green\" > Green </span><span id=\"blue\">Blue</span>"

let htmlData = Data(htmlString.utf8)

htmlString.htmlToAttributedString
htmlData.htmlToAttributedString

Discussion The HTML importer should not be called from a background thread (that is, the options dictionary includes documentType with a value of html). It will try to synchronize with the main thread, fail, and time out. Calling it from the main thread works (but can still time out if the HTML contains references to external resources, which should be avoided at all costs). The HTML import mechanism is meant for implementing something like markdown (that is, text styles, colors, and so on), not for general HTML import

Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
  • This is fantastic. I was able to convert `$` in an API to $. It's incredibly verbose though. Is there a more Swifty way? Thanks. – Edison Oct 02 '17 at 03:20
  • Tried it out. It's a wash because it's only eliminating three error lines `catch, print, return` but then you have to use `String(describing:` on your property anyway. Plus I should error check when using an API. Maybe Swift 5 will let us do it in one magic method like `encodeHTML` =P. – Edison Oct 02 '17 at 03:47
  • 1
    Yes. Functionality is same. I was trying to say that removing `catch`, `print`, and `return` is insignificant considering the tradeoff in safety. They all work. Very scary though :) – Edison Oct 02 '17 at 04:16
  • If I'm getting a zombie about 25% of the time I invoke this, what would you say I'm doing wrong? I'm not explicitly deallocating anywhere – froggomad May 02 '18 at 17:49
  • Can't we add HTML table ? ex.`let htmlTable = "
    Name Favorite Color
    Bob Yellow
    Michelle Purple
    "`
    – Jack Jul 13 '18 at 07:58
  • @LeoDabus i have shifted my code into `WebKit` which provide rich html features – Jack Apr 21 '19 at 11:09
23

Had this after automatic conversion to Swift 4. Was fixed by changing from:

NSMutableAttributedString(data: data, 
   options: [NSAttributedString.DocumentAttributeKey.documentType : NSAttributedString.DocumentType.html], 
   documentAttributes: nil)

to:

NSMutableAttributedString(data: data,
   options: [.documentType : NSAttributedString.DocumentType.html],
   documentAttributes: nil) {
Vitalii
  • 4,267
  • 1
  • 40
  • 45
14

This works for me:

let attrStr = try! NSAttributedString(
    data: modifiedFont.data(using: .unicode, allowLossyConversion: true)!,
    options:[.documentType: NSAttributedString.DocumentType.html,
    .characterEncoding: String.Encoding.utf8.rawValue],
    documentAttributes: nil)

If you don’t add

.characterEncoding: String.Encoding.utf8.rawValue

the app will crash.

LinusGeffarth
  • 27,197
  • 29
  • 120
  • 174
hall.keskin
  • 201
  • 3
  • 6
7

swift 4 : I dont know why all the answers have compiler error for me. so use this extension:

extension String {
    var html2AttributedString: NSAttributedString? {
        do {
            return try NSAttributedString(data: data(using: String.Encoding(rawValue: String.Encoding.utf8.rawValue))!,
                                          options: [.documentType: NSAttributedString.DocumentType.html,
                                                    .characterEncoding: String.Encoding.utf8.rawValue],
                                          documentAttributes: nil)
        } catch {
            print("error: ", error)
            return nil
        }
    }
    var html2String: String {
        return html2AttributedString?.string ?? ""
    }
}

how to use ?

mylable.text = htmlVariable.html2String

iman kazemayni
  • 1,255
  • 1
  • 19
  • 20
4

For HTML string, NSAttributedString.DocumentType.html is the correct option.

Swift 4

extension String {

    var utfData: Data? {
        return self.data(using: .utf8)
    }

    var htmlAttributedString: NSAttributedString? {
        guard let data = self.utfData else {
            return nil
        }
        do {
            return try NSAttributedString(data: data,
           options: [
                     NSAttributedString.documentType: NSAttributedString.DocumentType.html,
                     NSAttributedString.characterEncoding: String.Encoding.utf8.rawValue
                    ], documentAttributes: nil)
        } catch {
            print(error.localizedDescription)
            return nil
        }
    }
}
Community
  • 1
  • 1
Suhit Patil
  • 11,748
  • 3
  • 50
  • 60
2

Use NSAttributedString.DocumentType.html

NSMutableAttributedString(data: data, options: [.documentType: NSAttributedString.DocumentType.html] , documentAttributes: nil)
Bhanu
  • 1,249
  • 10
  • 17
0

I use NSAttributedStringKey and had similar error "Cannot convert value of type" on Swift 4. In case anyone using NSAttributedStringKey comes here looking for an answer, this is how I fixed:

let TextStroke: [NSAttributedStringKey : Any] = [
    NSAttributedStringKey(rawValue: NSAttributedStringKey.strokeColor.rawValue) : UIColor.black,
    NSAttributedStringKey(rawValue: NSAttributedStringKey.foregroundColor.rawValue) : UIColor.white,
    NSAttributedStringKey(rawValue: NSAttributedStringKey.strokeWidth.rawValue) : -6.0,]

And this is how I add the attribute to the text:

myLabel.attributedText = NSAttributedString(string: myString, attributes: TextStroke)
tomDev
  • 5,620
  • 5
  • 29
  • 39
  • 2
    This is horribly cumbersome. The benefit of `NSAttributedStringKey` is to avoid those long-winded initializations. You should simply write `let textStroke: [NSAttributedStringKey : Any] = [.strokeColor : UIColor.black, .foregroundColor : UIColor.white, .strokeWidth : -6.0] ` – vadian Sep 16 '17 at 18:08
0

Swift 4.x & 5.x

if let rtfPath = Bundle.main.url(forResource: "FileName", withExtension: "rtf") {
        do {
            let attributedString: NSAttributedString = try NSAttributedString(url: rtfPath, options: [NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.rtf], documentAttributes: nil)
            debugPrint(attributedString)
        } catch {
            print("Error while reading the file - \(error.localizedDescription)")
        }
 }

Getting NSAttributedString from file will be changed in Swift 3.x which is as below:

let attributedStringWithRtf: NSAttributedString = try NSAttributedString(url: rtfPath, options: [NSDocumentTypeDocumentAttribute:NSRTFTextDocumentType], documentAttributes: nil)

all the other code same as Swift 4.x & 5.x.

For more details please read the Apple document related to the NSAttributedStringDocumentType

yo2bh
  • 1,356
  • 1
  • 14
  • 26