164

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.

Joshua
  • 15,200
  • 21
  • 100
  • 172
  • 2
    The NSAttributedString-Additions-for-HTML library has been renamed and rolled into a framework by the same author. It's now called DTCoreText and includes a bunch of Core Text layout classes. You can find it over [here](https://github.com/Cocoanetics/DTCoreText) – Brian Douglas Moakley Jan 04 '12 at 14:49

18 Answers18

309

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)
Igor Makarov
  • 699
  • 5
  • 7
pix
  • 5,052
  • 2
  • 23
  • 25
  • anything similar for iOS6? – Lirik Sep 29 '13 at 14:08
  • @Lirik I think in iOS6, your best bet is the DTCoreText answer above. We are crossing our fingers and hoping our userbase have all updated by the time we launch ;) – pix Oct 01 '13 at 02:42
  • 31
    For some reason, the option NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType is causing the encoding to take a really, really long time :( – Arie Litovsky Oct 04 '13 at 19:06
  • @diablosnuevos it doesn't seem to take a noticeably long time in our app, but we are only dealing with a magazine article (about 1 page of simple markup). – pix Oct 07 '13 at 11:40
  • 1
    @pix, I realized what my issue was – I was calling the method from a UIWebview delegate callback (webviewDidLoad:), and I believe that it was causing a livelock because of the WebKit threads. – Arie Litovsky Oct 07 '13 at 16:31
  • 1
    @diablosnuevos If you are in a UIWebView, why were you converting HTML to NSAttributedString? – pix Oct 08 '13 at 04:49
  • 17
    Too bad NSHTMLTextDocumentType is (literally) ~1000x slower than setting attributes with NSRange. (Profiled a short label with one bold tag.) – Jason Moore Oct 23 '13 at 16:28
  • @pix do you have any idea if I have html with CSS style, how this can be solved? Without using DTCoreText – Natan R. Oct 24 '13 at 11:31
  • 6
    Be aware that if you can't NSHTMLTextDocumentType with this method if you want to use it from a background thread. Even with ios 7, it won't use TextKit for the HTML rendering. Take a look at the DTCoreText library recommended by Ingve. – TJez Nov 12 '13 at 18:27
  • 2
    Awesome. Just a thought, you could probably do [NSNumber numberWithInt:NSUTF8StringEncoding] as @(NSUTF8StringEncoding), no? – Jarsen Dec 19 '13 at 16:51
  • Love this approach, if you can afford to limit support to just iOS7. – Adam Simpson Mar 19 '14 at 17:23
  • Another hidden gem in iOS7. – Hubert Kunnemeyer May 10 '14 at 16:25
  • 17
    I was doing this, but be careful on iOS 8. It's painfully slow, close to a second for a few hundred characters. (In iOS 7 it was nearly instantaneous.) – Norman Sep 16 '14 at 21:01
  • if only I could vote up twice! It is slow but it does the business. Best solution perhaps to parse off main thread? – Max MacLeod Sep 26 '14 at 13:34
  • For me app is crashing with EXC_BAD_ACCESS at this method. I am assigning attributed string to UILabel of a UITableViewCell. Also, to remove the sluggishness in cell reuse, I kept the code in global queue block. Any suggestions as to what should be done so that it doesn't crash like that? – tech savvy Jan 07 '15 at 06:53
  • 2
    i had the crash problem too it was because i wasnt doing the initWithData for the atributed string on the main thread – Bogdan Jan 30 '15 at 12:06
  • This is so painfully slow in iOS 8, I actually used DTCoreText instead. – Enrico Susatyo Mar 22 '15 at 10:14
  • ((NSAttributedString*)obj).string can get the normal string. – zszen Jan 15 '18 at 08:25
  • For this approach, changing text attributes for each NSRange in the resulted NSMutableAttributedString is painful also. We can re-format the text by appending CSS "" before converting it to NSMutableAttributedString for displaying. – Hoai Dam Sep 13 '18 at 11:30
  • Also, performance is better on iOS 12, around 0.5-0.6 second for ~900KB of both text+data_url_image (ran on ... iPhone 5S). – Hoai Dam Sep 13 '18 at 11:34
  • @HoaiDam I tested this on iOS 12 with a table view, and scrolling was very choppy. I believe the Foundation method is still using the main thread, even though it may be faster now. If you have lots of HTML strings and care about scrolling performance, you are better off parsing the HTML yourself and building the string yourself. – ray Oct 17 '18 at 19:48
  • @ray Yups, it is choppy if you try to parse HTML 'on the fly' while rendering `UITableViewCell`. Here's a related question: https://stackoverflow.com/questions/21166752/why-does-the-initial-call-to-nsattributedstring-with-an-html-string-take-over-10 . I think it's better to parse the HTML text on the background ONLY ONCE, and then save (or cache) the generated `NSAttributedString` for later use. – Hoai Dam Oct 19 '18 at 02:43
  • 2
    Warning to anybody considering this solution: This API is cursed and will unleash Zalgo throughout your app. (https://blog.izs.me/2013/08/designing-apis-for-asynchrony) The API executes the main runloop *before returning*, meaning that if you put a block on the main queue before calling it, that block will potentially be executed before it returns. Here's a good explanation: https://stackoverflow.com/questions/56154827/nsattributedstring-from-html-on-main-thread-behaves-as-if-multithreading Good luck reasoning about your code if you use this API! – n8gray Nov 19 '19 at 19:07
  • Another Warning (from Apple docs): "...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." – shallowThought Sep 18 '20 at 13:05
  • is this will load image tags in html string ? – Vinayak Mar 25 '22 at 06:03
47

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

Ingve
  • 1,086
  • 2
  • 20
  • 39
  • Requires min deployment of iOS 4.3 :( None-the-less, very impressive. – Oh Danny Boy Mar 20 '12 at 17:37
  • 3
    @Lirik Overkill for you maybe but perfect for someone else i.e. your comment isn't in the least bit helpful. – wuf810 Nov 04 '14 at 09:41
  • 3
    Please note that this project requires is open source and covered by a standard 2-clause BSD license. That means you have to mention Cocoanetics as the original author of this code and reproduce the LICENSE text inside your app. – Dulgan Jul 27 '15 at 10:01
34

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

Andrew Schreiber
  • 14,344
  • 6
  • 46
  • 53
  • Andrew. This is working fine. I wanted to know what all short of events that i have to handle in my UITextView if i will go with this approach. Can it handle Calendar event , Call, Email, Website link etc available in HTML? I hope UITextView is able to handle events compare to UILabel. – harshit2811 Feb 11 '16 at 12:23
  • The above approach is only good for formatting. I would recommend using [TTTAttributedLabel](https://github.com/TTTAttributedLabel/TTTAttributedLabel) if you need event handling. – Andrew Schreiber Feb 22 '16 at 21:00
  • The default encoding that NSAttributedString uses is NSUTF16StringEncoding (not UTF8!). That's why this will not work. At least in my case! – Umit Kaya Jan 19 '17 at 16:36
  • This should be the accepted solution. Doing an HTML string conversation on a background thread *will* eventually crash, and quite frequently while running tests. – ratsimihah Mar 20 '18 at 18:04
23

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)
Hashem Aboonajmi
  • 13,077
  • 8
  • 66
  • 75
Mobile Dan
  • 6,444
  • 1
  • 44
  • 44
12

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

samwize
  • 25,675
  • 15
  • 141
  • 186
6

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
}
fssilva
  • 995
  • 11
  • 7
6

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 }
Community
  • 1
  • 1
He Yifei 何一非
  • 2,592
  • 4
  • 38
  • 69
6

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!")
AamirR
  • 11,672
  • 4
  • 59
  • 73
4

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

Pavel Sharanda
  • 919
  • 10
  • 9
3

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.

jer
  • 20,094
  • 5
  • 45
  • 69
  • 1
    If the HTML is XHTML-Strict, you could use NSXMLDOcument and friends to help with the parsing. – Dylan Lukes Nov 18 '10 at 18:37
  • How would you suggest I go about building up the nodes with given attributes? – Joshua Nov 18 '10 at 19:11
  • 2
    That's an implementation detail. However you parse the HTML, you have access to each attribute for each tag, which specifies things like a font name, size, etc. You can use this information to store the relevant details you'd need to add to the attributed text as attributes. Generally, you need to get yourself familiar with parsing first before tackling such a task. – jer Nov 18 '10 at 21:27
3

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()
reza_khalafi
  • 6,230
  • 7
  • 56
  • 82
2

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.

alecxe
  • 462,703
  • 120
  • 1,088
  • 1,195
  • @alecex I did meet the same problem! app will crash on iOS 8.1, 2, 3. But will be fine on iOS 8.4 or later. Can you explain in detail how to avoid it? or is there any work around, or methods can be used instead? – Strong84 Jan 01 '16 at 09:06
  • I made a quick category to handle this, copying the methods from AppKit, which has a very easy and intuitive way to do this. Why Apple didn't add it is beyond me.: https://github.com/cguess/NSMutableAttributedString-HTML – CGuess Feb 02 '16 at 20:31
1

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)
    }
}
Stephen Orr
  • 291
  • 3
  • 7
1

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)
    }
}
S1LENT WARRIOR
  • 11,704
  • 4
  • 46
  • 60
0

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 }
AmitaiB
  • 1,656
  • 1
  • 19
  • 19
0

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.

Anton Tropashko
  • 5,486
  • 5
  • 41
  • 66
0

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
Isha
  • 1
  • 1
0

Add this Extension and then use Text. after using this code we can use custom size of our text.

extension Text {
    init(html htmlString: String,
         raw: Bool = false,
         size: CGFloat? = nil,
         fontFamily: String = "-apple-system") {
        let fullHTML: String
        if raw {
            fullHTML = htmlString
        } else {
            var sizeCss = ""
            if let size = size {
                sizeCss = "font-size: \(size)px;"
            }
            fullHTML = """
            <!doctype html>
            <html>
              <head>
                <style>
                  body {
                    font-family: \(fontFamily);
                    \(sizeCss)
                  }
                </style>
              </head>
              <body>
                \(htmlString)
              </body>
            </html>
            """
        }
        let attributedString: NSAttributedString
        if let data = fullHTML.data(using: .unicode),
           let attrString = try? NSAttributedString(data: data,
                                                    options: [.documentType: NSAttributedString.DocumentType.html],
                                                    documentAttributes: nil) {
            attributedString = attrString
        } else {
            attributedString = NSAttributedString()
        }

        self.init(attributedString)
    }

    init(_ attributedString: NSAttributedString) {
        self.init("")

        attributedString.enumerateAttributes(in: NSRange(location: 0, length: attributedString.length), options: []) { (attrs, range, _) in
            let string = attributedString.attributedSubstring(from: range).string
            var text = Text(string)

            if let font = attrs[.font] as? UIFont {
                text = text.font(.init(font))
            }

            if let color = attrs[.foregroundColor] as? UIColor {
                text = text.foregroundColor(Color(color))
            }

            if let kern = attrs[.kern] as? CGFloat {
                text = text.kerning(kern)
            }

            if #available(iOS 14.0, *) {
                if let tracking = attrs[.tracking] as? CGFloat {
                    text = text.tracking(tracking)
                }
            }

            if let strikethroughStyle = attrs[.strikethroughStyle] as? NSNumber, strikethroughStyle != 0 {
                if let strikethroughColor = (attrs[.strikethroughColor] as? UIColor) {
                    text = text.strikethrough(true, color: Color(strikethroughColor))
                } else {
                    text = text.strikethrough(true)
                }
            }

            if let underlineStyle = attrs[.underlineStyle] as? NSNumber,
               underlineStyle != 0 {
                if let underlineColor = (attrs[.underlineColor] as? UIColor) {
                    text = text.underline(true, color: Color(underlineColor))
                } else {
                    text = text.underline(true)
                }
            }

            if let baselineOffset = attrs[.baselineOffset] as? NSNumber {
                text = text.baselineOffset(CGFloat(baselineOffset.floatValue))
            }

            self = self + text
        }
    }
}