A Swift extension for String and HTML

Nov 08, 2017 · 14 mins read · Costantino Pistagna · @valv0

A Swift extension for String and HTML

One of the most common daily request received is to manage html strings from a backend, rendering them correctly within a UILabel. In most cases an iOS app needs to coexist with a web counterpart that parse HTML more efficiently than an UILabel.

All this was true before the advent of the NSAttributedString that was a game changer at all. UILabel objects can also accept a particular value within their properties named attributedText:


let aLabel = UILabel()
aLabel.attributedText = NSAttributedString(string: hello, world!)

Obviously, it’s not enough to stick HTML text as the value of this property … otherwise there would be no reason to write this post.


In the beginning were DIY extensions that made it possible to parse HTML tags, transforming them into attributes for our NSAttributedString. Specifically, I fallen in love with a particular one of these extensions. I employed it many times in the past. Then Swift came out and this extension was not up to date. In the very beginning, when projects were not so pure containing Objective-C and Swift, I continued to deal with it. However, over the past two years, things have become a bit more complex because my projects are now completely swift based and I don’t want to deal anymore with “hybrid” code.

Of course, there are many other similar extensions written in Swift, but none of them gave me a “wow effect” as the one above for many reasons: complexity, dependency management, and other factors. In short, I had to find a different way.

By accident I came across Dejan Atanasov’s post. It turn out that it is possible to manage the HTML directly with native Swift components. Let’s take a look at the core operation and then I will show you how I improved Dejan’s suggestion.

NSAttributedString provides a constructor that take in input the RAW version of the string in binary data format and some other options.


NSAttributedString(data:options:documentAttributes:)

Moreover — here the magic happens — if we instruct NSAttributedString to use the raw input as an HTML document, the result will be a correctly initialized object with all the possible attributes directly extracted from HTML tags. Awesome. Just what we were looking for.

For the more curious, there is a further option. It is possible to return an object containing a dictionary of all the parameters used by the html converter for later use (for example, it might be useful to align other contents within the UIView that are not part of the HTML page).

Let’s see how to build this call:


extension String {
    var html2Attributed: NSAttributedString? {
        do {
            guard let data = data(using: String.Encoding.utf8) else {
                return nil
            }
            return try NSAttributedString(data: data,
                                          options: [.documentType: NSAttributedString.DocumentType.html,
                                                    .characterEncoding: String.Encoding.utf8.rawValue],
                                          documentAttributes: nil)
        } catch {
            print("error: ", error)
            return nil
        }
    }
}

What’s going on? We have extended the behavior of the String object to handle the conversion we are interested in. Firstly, we ensure that we can convert our string to data; if the operation fails for some reason, we will return nil. Otherwise, let’s proceed with a try construct that in turn will return the value we are interested in. The try catch construct allows us to handle any exceptions and return a nil just in case of errors.

The NSAttributedString init method (data: options: documentAttributes:) take the binary value of the string as Data object and an optional dictionary. This dictionary specifies that the input Data .documentType is HTML NSAttributedString.DocumentType.html as well as its encoding .characterEncoding: String.Encoding.utf8.rawValue.

So far, so good. At this point we are able to convert HTML into NSAttributedString with a single step:


aLabel.attributedText = <b>Hello</b>,<i>world</i>.html2Attributed

Awesome! By the way, there are still two major issues that need to be addressed:

  • You will notice that the text is rendered with a default font that is seriously different from the one we are accustomed to by iOS11.

  • There is a parameter we didn’t take in consideration and that was set to nil (documentAttributed).


Let’s start with the first problem. HTML text is rendered by default with the Times New Roman font. To overcome this problem there are many solutions and it depends also on what we want to get as well as the context of our problem. Since we are dealing with HTML, a fairly flexible solution involves the use of CSS and global html style. If we were in the presence of a simple HTML page with only our text, we could use a CSS similar to the following to force the text style across the entire HTML page:


<style>
html * {font-size: 12.0pt !important;color: #383838 !important;font-family: Helvetica !important;}
</style>
<b>hello</b>, <i>world</i>

Porting this concept to swift extention:


import UIKit
import Foundation

extension String {
    func htmlAttributed(family: String?, size: CGFloat, color: UIColor) -> NSAttributedString? {
        do {
            let htmlCSSString = "<style>" +
                "html *" +
                "{" +
                "font-size: \(size)pt !important;" +
                "color: #\(color.hexString!) !important;" +
                "font-family: \(family ?? "Helvetica"), Helvetica !important;" +
            "}</style> \(self)"

            guard let data = htmlCSSString.data(using: String.Encoding.utf8) else {
                return nil
            }

            return try NSAttributedString(data: data,
                                          options: [.documentType: NSAttributedString.DocumentType.html,
                                                    .characterEncoding: String.Encoding.utf8.rawValue],
                                          documentAttributes: nil)
        } catch {
            print("error: ", error)
            return nil
        }
    }
}

Much much better! Now our HTML text is inserted into a more complex HTML context that involves using a CSS. Note how the font-size, color, and font-family are parameterized using the input parameters, making the method quite general to use in various circumstances.

It is worth noting how we handle UIColor. The latter, does not contain a built-in extension to handle the hex conversion of the specified color:


import UIKit
import Foundation

extension UIColor {
    var hexString:String? {
        if let components = self.cgColor.components {
            let r = components[0]
            let g = components[1]
            let b = components[2]
            return  String(format: "%02X%02X%02X", (Int)(r * 255), (Int)(g * 255), (Int)(b * 255))
        }
        return nil
    }
}

Lastly but not least, there is the last parameter documentAttributes that we didn’t use, yet. But what is it for? This parameter works by reference (not by value) and at the end of the function it is populated with some useful information about attributed text that has just been created. These data can be useful to know the actual screen rendering area and other service information that would otherwise be difficult to retrieve.


extension String {
    var htmlAttributed: (NSAttributedString?, NSDictionary?) {
        do {
            guard let data = data(using: String.Encoding.utf8) else {
                return (nil, nil)
            }

            var dict:NSDictionary?
            dict = NSMutableDictionary()

            return try (NSAttributedString(data: data,
                                          options: [.documentType: NSAttributedString.DocumentType.html,
                                                    .characterEncoding: String.Encoding.utf8.rawValue],
                                          documentAttributes: &dict), dict)
        } catch {
            print("error: ", error)
            return (nil, nil)
        }
    }
}

This time the extension returns a tuple and not a single value. In this way we can handle the two important values in one single pass: the attributes dictionary and its NSAttributedString.

It is important to note how the dictionary is first built and then passed for reference & to the initialization method. This way, when we return from the constructor, we will have the object properly valorized.


Put everything together. The complete extension that offer various methods to deal with different situations of string can be found here:


//
//  String+HTML.swift
//  AttributedString
//
//  Created by Costantino Pistagna on 08/11/2017.
//  Copyright © 2017 sofapps.it All rights reserved.
//
import UIKit
import Foundation

extension UIColor {
    var hexString:String? {
        if let components = self.cgColor.components {
            let r = components[0]
            let g = components[1]
            let b = components[2]
            return  String(format: "%02X%02X%02X", (Int)(r * 255), (Int)(g * 255), (Int)(b * 255))
        }
        return nil
    }
}

extension String {
    var html2Attributed: NSAttributedString? {
        do {
            guard let data = data(using: String.Encoding.utf8) else {
                return nil
            }
            return try NSAttributedString(data: data,
                                          options: [.documentType: NSAttributedString.DocumentType.html,
                                                    .characterEncoding: String.Encoding.utf8.rawValue],
                                          documentAttributes: nil)
        } catch {
            print("error: ", error)
            return nil
        }
    }
    
    var htmlAttributed: (NSAttributedString?, NSDictionary?) {
        do {
            guard let data = data(using: String.Encoding.utf8) else {
                return (nil, nil)
            }

            var dict:NSDictionary?
            dict = NSMutableDictionary()

            return try (NSAttributedString(data: data,
                                          options: [.documentType: NSAttributedString.DocumentType.html,
                                                    .characterEncoding: String.Encoding.utf8.rawValue],
                                          documentAttributes: &dict), dict)
        } catch {
            print("error: ", error)
            return (nil, nil)
        }
    }
    
    func htmlAttributed(using font: UIFont, color: UIColor) -> NSAttributedString? {
        do {
            let htmlCSSString = "<style>" +
                "html *" +
                "{" +
                "font-size: \(font.pointSize)pt !important;" +
                "color: #\(color.hexString!) !important;" +
                "font-family: \(font.familyName), Helvetica !important;" +
                "}</style> \(self)"

            guard let data = htmlCSSString.data(using: String.Encoding.utf8) else {
                return nil
            }

            return try NSAttributedString(data: data,
                                          options: [.documentType: NSAttributedString.DocumentType.html,
                                                    .characterEncoding: String.Encoding.utf8.rawValue],
                                          documentAttributes: nil)
        } catch {
            print("error: ", error)
            return nil
        }
    }

    func htmlAttributed(family: String?, size: CGFloat, color: UIColor) -> NSAttributedString? {
        do {
            let htmlCSSString = "<style>" +
                "html *" +
                "{" +
                "font-size: \(size)pt !important;" +
                "color: #\(color.hexString!) !important;" +
                "font-family: \(family ?? "Helvetica"), Helvetica !important;" +
            "}</style> \(self)"

            guard let data = htmlCSSString.data(using: String.Encoding.utf8) else {
                return nil
            }

            return try NSAttributedString(data: data,
                                          options: [.documentType: NSAttributedString.DocumentType.html,
                                                    .characterEncoding: String.Encoding.utf8.rawValue],
                                          documentAttributes: nil)
        } catch {
            print("error: ", error)
            return nil
        }
    }
}

References


Costantino Pistagna
Costantino Pistagna · @valv0 Costantino is a software architect, project manager and consultant with more than ten years of experience in the software industry. He developed and managed projects for universities, medium-sized companies, multi-national corporations, and startups. He is among the first teachers for the Apple's iOS Developer Academy, based in Europe. Regularly, he lectures iOS development around the world, giving students the skills to develop their own high quality apps. While not writing apps, Costantino improves his chefs skills, travelling the world with his beautiful family.