18

I am dinamically getting an HTML string from a Wordpress API and parsing it into an Attributed String to show it in my app. Since the string has its own styles, it shows different fonts and sizes, something that is affecting our design choices.

What I want to do is change the font and its size on the whole attributed string.

I tried doing so in the options of the attributed string, but it does nothing:

let attributedT = try! NSAttributedString(
            data: nContent!.decodeHTML().data(using: String.Encoding.unicode, allowLossyConversion: true)!,
            options: [ NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType, NSFontAttributeName: UIFont(name: "Helvetica", size: 16.0)!],
            documentAttributes: nil)
        contentLbl.attributedText = attributedT

Does anybody have any ideas on how to achieve this?

P.S. I know that I could add a CSS tag to the beginning or end of the string, but would this override other styles in it? Also, if this is a valid solution, could you please provide a sample on how to do it?

Jacobo Koenig
  • 11,728
  • 9
  • 40
  • 75
  • Hint (not pointed out but current answers): The doc of `NSAttributedString(data:options:documentAttributes:)` states that you can't put `NSFontAttributeName` in `options`, it won't be read. That's why you code doesn't do what you expect. – Larme Jan 04 '17 at 09:29

8 Answers8

29

Swift 4 solution


  • NSAttributedString extension with convenience initializer
  • Enumerates through the attributed string (HTML document) font attributes, and replaces with the provided UIFont
  • Preserves original HTML font sizes, or uses font-size from provided UIFont, @see useDocumentFontSize parameter
  • This method can simply convert HTML to NSAttributedString, without the overload of manipulating with fonts, just skip the font parameter, @see guard statement

extension NSAttributedString {

    convenience init(htmlString html: String, font: UIFont? = nil, useDocumentFontSize: Bool = true) throws {
        let options: [NSAttributedString.DocumentReadingOptionKey : Any] = [
            .documentType: NSAttributedString.DocumentType.html,
            .characterEncoding: String.Encoding.utf8.rawValue
        ]

        let data = html.data(using: .utf8, allowLossyConversion: true)
        guard (data != nil), let fontFamily = font?.familyName, let attr = try? NSMutableAttributedString(data: data!, options: options, documentAttributes: nil) else {
            try self.init(data: data ?? Data(html.utf8), options: options, documentAttributes: nil)
            return
        }

        let fontSize: CGFloat? = useDocumentFontSize ? nil : font!.pointSize
        let range = NSRange(location: 0, length: attr.length)
        attr.enumerateAttribute(.font, in: range, options: .longestEffectiveRangeNotRequired) { attrib, range, _ in
            if let htmlFont = attrib as? UIFont {
                let traits = htmlFont.fontDescriptor.symbolicTraits
                var descrip = htmlFont.fontDescriptor.withFamily(fontFamily)

                if (traits.rawValue & UIFontDescriptorSymbolicTraits.traitBold.rawValue) != 0 {
                    descrip = descrip.withSymbolicTraits(.traitBold)!
                }

                if (traits.rawValue & UIFontDescriptorSymbolicTraits.traitItalic.rawValue) != 0 {
                    descrip = descrip.withSymbolicTraits(.traitItalic)!
                }

                attr.addAttribute(.font, value: UIFont(descriptor: descrip, size: fontSize ?? htmlFont.pointSize), range: range)
            }
        }

        self.init(attributedString: attr)
    }

}

Usage-1 (Replace font)

let attr = try? NSAttributedString(htmlString: "<strong>Hello</strong> World!", font: UIFont.systemFont(ofSize: 34, weight: .thin))

Usage-2 (NSMutableAttributedString example)

let attr = try! NSMutableAttributedString(htmlString: "<strong>Hello</strong> World!", font: UIFont.systemFont(ofSize: 34, weight: .thin))
attr.append(NSAttributedString(string: " MINIMIZE", attributes: [.link: "@m"]))

Usage-3 (Only convert HTML to NSAttributedString)

let attr = try? NSAttributedString(htmlString: "<strong>Hello</strong> World!")
AamirR
  • 11,672
  • 4
  • 59
  • 73
  • I'm getting `'DocumentReadingOptionKey' is unavailable in Swift`. Have they replaced it with something else since then? Google search turns up nothing. – keverly Feb 19 '18 at 23:00
  • @keverly As mentioned ***Swift 4 solution*** (see first line), and you are using Swift 3.x, please check my new answer for a ***Swift 3*** equivalent – AamirR Feb 20 '18 at 09:11
  • Thanks for the response. I only asked because I went to "Project" -> "Build Settings" -> "Swift Language version" and it says 4. Must be a different issue though. – keverly Feb 21 '18 at 04:47
21

What you want to do, basically, is turn the NSAttributedString into an NSMutableAttributedString.

let attributedT = // ... attributed string
let mutableT = NSMutableAttributedString(attributedString:attributedT)

Now you can call addAttributes to apply attributes, such as a different font, over any desired range, such as the whole thing.

Unfortunately, however, a font without a symbolic trait such as italic is a different font from a font with that symbolic trait. Therefore, you're going to need a utility that copies the existing symbolic traits from a font and applies them to another font:

func applyTraitsFromFont(_ f1: UIFont, to f2: UIFont) -> UIFont? {
    let t = f1.fontDescriptor.symbolicTraits
    if let fd = f2.fontDescriptor.withSymbolicTraits(t) {
        return UIFont.init(descriptor: fd, size: 0)
    }
    return nil
}

Okay, so, armed with that utility, let's try it. I'll start with some simple HTML and convert it to an attributed string, just as you are doing:

let html = "<p>Hello <i>world</i>, hello</p>"
let data = html.data(using: .utf8)!
let att = try! NSAttributedString.init(
    data: data, options: [NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType],
    documentAttributes: nil)
let matt = NSMutableAttributedString(attributedString:att)

As you can see, I've converted to an NSMutableAttributedString, as I advised. Now I'll cycle thru the style runs in terms of font, altering to a different font while using my utility to apply the existing traits:

matt.enumerateAttribute(
    NSFontAttributeName,
    in:NSMakeRange(0,matt.length),
    options:.longestEffectiveRangeNotRequired) { value, range, stop in
        let f1 = value as! UIFont
        let f2 = UIFont(name:"Georgia", size:20)!
        if let f3 = applyTraitsFromFont(f1, to:f2) {
            matt.addAttribute(
                NSFontAttributeName, value:f3, range:range)
        }
    }

Here's the result:

enter image description here

Obviously you could tweak this procedure to be even more sophisticated, depending on your design needs.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • The setAttributes will reset all the attributes from HTML. – bubuxu Jan 01 '17 at 03:17
  • 1
    Right, I meant `addAttributes`. Fixed now. Thanks for catching that. – matt Jan 01 '17 at 03:26
  • I am using this method. I am facing an issue. Whenever I pass a custom font added in my app to this: let f2 = UIFont(name:"Georgia", size:20)! . Font style doesn't work, but size works. It works with the othe font families that are in xcode. – SNarula Mar 14 '17 at 13:24
  • @SNarula If you have a question, ask a _question_. – matt Mar 14 '17 at 13:57
  • I want to ask about this line let f2 = UIFont(name:"Georgia", size:20)!. Font name in this line only works for the font that are in Xcode. Its not work with custom fonts. Why? I am using let f2 = UIFont(name:"Montserrat-ExtraLight", size: 10.0)! but it does not give me desired result. – SNarula Mar 15 '17 at 11:31
  • @SNarula Then ask. Not in comments. As a question. Click Ask Question at top right, and ask. – matt Mar 15 '17 at 13:34
  • 1
    This solution worked perfectly. But I am surprised that for a simple task such a long code was required. – zeeshan Jan 19 '19 at 17:02
  • @PepengHapon it “comes” from the 4th paragraph of the answer. – matt Oct 26 '19 at 03:34
  • If it has img tag, does it crash? – Twitter khuong291 Dec 11 '20 at 10:31
17

The setAttributes will reset all the attributes from HTML. I wrote an extension method to avoid this:

Swift 4

public convenience init?(HTMLString html: String, font: UIFont? = nil) throws {
    let options : [NSAttributedString.DocumentReadingOptionKey : Any] =
        [NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.html,
         NSAttributedString.DocumentReadingOptionKey.characterEncoding: String.Encoding.utf8.rawValue]

    guard let data = html.data(using: .utf8, allowLossyConversion: true) else {
        throw NSError(domain: "Parse Error", code: 0, userInfo: nil)
    }

    if let font = font {
        guard let attr = try? NSMutableAttributedString(data: data, options: options, documentAttributes: nil) else {
            throw NSError(domain: "Parse Error", code: 0, userInfo: nil)
        }
        var attrs = attr.attributes(at: 0, effectiveRange: nil)
        attrs[NSAttributedStringKey.font] = font
        attr.setAttributes(attrs, range: NSRange(location: 0, length: attr.length))
        self.init(attributedString: attr)
    } else {
        try? self.init(data: data, options: options, documentAttributes: nil)
    }
}

Test sample:

let html = "<html><body><h1 style=\"color:red;\">html text here</h1></body></html>"
let font = UIFont.systemFont(ofSize: 16)

var attr = try NSMutableAttributedString(HTMLString: html, font: nil)
var attrs = attr?.attributes(at: 0, effectiveRange: nil)
attrs?[NSAttributedStringKey.font] as? UIFont
// print: <UICTFont: 0x7ff19fd0a530> font-family: "TimesNewRomanPS-BoldMT"; font-weight: bold; font-style: normal; font-size: 24.00pt

attr = try NSMutableAttributedString(HTMLString: html, font: font)
attrs = attr?.attributes(at: 0, effectiveRange: nil)
attrs?[NSAttributedStringKey.font] as? UIFont
// print: <UICTFont: 0x7f8c0cc04620> font-family: ".SFUIText"; font-weight: normal; font-style: normal; font-size: 16.00pt
markshiz
  • 2,501
  • 22
  • 28
bubuxu
  • 2,187
  • 18
  • 23
  • Please change "text" in the extension for "html". This will make the answer complete and correct. – Jacobo Koenig Jan 02 '17 at 22:00
  • 1
    @bubuxu, I see the same log (correct font family, font type etc) as you mentioned here but when I set attr to UILabel it doesn't work. – Harsh4789 Jun 27 '18 at 09:45
5

Swift 3 version of my previous (Swift 4) solution


extension NSAttributedString {

    convenience init(htmlString html: String, font: UIFont? = nil, useDocumentFontSize: Bool = true) throws {
        let options: [String : Any] = [
            NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType,
            NSCharacterEncodingDocumentAttribute: String.Encoding.utf8.rawValue
        ]

        let data = html.data(using: .utf8, allowLossyConversion: true)
        guard (data != nil), let fontFamily = font?.familyName, let attr = try? NSMutableAttributedString(data: data!, options: options, documentAttributes: nil) else {
            try self.init(data: data ?? Data(html.utf8), options: options, documentAttributes: nil)
            return
        }

        let fontSize: CGFloat? = useDocumentFontSize ? nil : font!.pointSize
        let range = NSRange(location: 0, length: attr.length)
        attr.enumerateAttribute(NSFontAttributeName, in: range, options: .longestEffectiveRangeNotRequired) { attrib, range, _ in
            if let htmlFont = attrib as? UIFont {
                let traits = htmlFont.fontDescriptor.symbolicTraits
                var descrip = htmlFont.fontDescriptor.withFamily(fontFamily)

                if (traits.rawValue & UIFontDescriptorSymbolicTraits.traitBold.rawValue) != 0 {
                    descrip = descrip.withSymbolicTraits(.traitBold)!
                }

                if (traits.rawValue & UIFontDescriptorSymbolicTraits.traitItalic.rawValue) != 0 {
                    descrip = descrip.withSymbolicTraits(.traitItalic)!
                }

                attr.addAttribute(NSFontAttributeName, value: UIFont(descriptor: descrip, size: fontSize ?? htmlFont.pointSize), range: range)
            }
        }

        self.init(attributedString: attr)
    }

}
AamirR
  • 11,672
  • 4
  • 59
  • 73
3

Just wanted to thanks @AamirR for his response and warn other future users about a small bug with the code.

If you use it you might face a problem with bold AND italic strings where only one of this traits are used in the end. This is the same code with that bug fixed, hope it helps:

extension NSAttributedString {

convenience init(htmlString html: String, font: UIFont? = nil, useDocumentFontSize: Bool = true) throws {
    let options: [String : Any] = [
        NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType,
        NSCharacterEncodingDocumentAttribute: String.Encoding.utf8.rawValue
    ]

    let data = html.data(using: .utf8, allowLossyConversion: true)
    guard (data != nil), let fontFamily = font?.familyName, let attr = try? NSMutableAttributedString(data: data!, options: options, documentAttributes: nil) else {
        try self.init(data: data ?? Data(html.utf8), options: options, documentAttributes: nil)
        return
    }

    let fontSize: CGFloat? = useDocumentFontSize ? nil : font!.pointSize
    let range = NSRange(location: 0, length: attr.length)
    attr.enumerateAttribute(NSFontAttributeName, in: range, options: .longestEffectiveRangeNotRequired) { attrib, range, _ in
        if let htmlFont = attrib as? UIFont {
            var traits = htmlFont.fontDescriptor.symbolicTraits
            var descrip = htmlFont.fontDescriptor.withFamily(fontFamily)

            if (traits.rawValue & UIFontDescriptorSymbolicTraits.traitBold.rawValue) != 0 {
                traits = traits.union(.traitBold)
            }

            if (traits.rawValue & UIFontDescriptorSymbolicTraits.traitItalic.rawValue) != 0 {
                traits = traits.union(.traitItalic)
            }

            descrip = descrip.withSymbolicTraits(traits)!
            attr.addAttribute(NSFontAttributeName, value: UIFont(descriptor: descrip, size: fontSize ?? htmlFont.pointSize), range: range)
        }
    }

    self.init(attributedString: attr)
  }
}
juavazmor
  • 73
  • 1
  • 6
2
let font = "<font face='Montserrat-Regular' size='13' color= 'black'>%@"
let html = String(format: font, yourhtmlstring)
webView.loadHTMLString(html, baseURL: nil)
urvashi bhagat
  • 1,123
  • 12
  • 15
2

Thanks @AamirR for the post. Also I think adding extra "useDocumentFontSize" is not necessary. If you don't want to change font, send font parameter just nil.

Swift 5 version :

extension NSAttributedString { convenience init(htmlString html: String, font: UIFont? = nil) throws {
    let options: [NSAttributedString.DocumentReadingOptionKey : Any] = [
        .documentType: NSAttributedString.DocumentType.html,
        .characterEncoding: String.Encoding.utf8.rawValue
    ]

    let data = html.data(using: .utf8, allowLossyConversion: true)
    guard (data != nil), let fontFamily = font?.familyName, let attr = try? NSMutableAttributedString(data: data!, options: options, documentAttributes: nil) else {
        try self.init(data: data ?? Data(html.utf8), options: options, documentAttributes: nil)
        return
    }

    let fontSize: CGFloat? = font == nil ? nil : font!.pointSize
    let range = NSRange(location: 0, length: attr.length)
    attr.enumerateAttribute(.font, in: range, options: .longestEffectiveRangeNotRequired) { attrib, range, _ in
        if let htmlFont = attrib as? UIFont {
            let traits = htmlFont.fontDescriptor.symbolicTraits
            var descrip = htmlFont.fontDescriptor.withFamily(fontFamily)

            if (traits.rawValue & UIFontDescriptor.SymbolicTraits.traitBold.rawValue) != 0 {
                descrip = descrip.withSymbolicTraits(.traitBold)!
            }

            if (traits.rawValue & UIFontDescriptor.SymbolicTraits.traitItalic.rawValue) != 0 {
                descrip = descrip.withSymbolicTraits(.traitItalic)!
            }

            attr.addAttribute(.font, value: UIFont(descriptor: descrip, size: fontSize ?? htmlFont.pointSize), range: range)
        }
    }

    self.init(attributedString: attr)
}
xevser
  • 369
  • 4
  • 5
-2
let font = UIFont(name: fontName, size: fontSize)
textAttributes[NSFontAttributeName] = font
self.attributedText = NSAttributedString(string: self.text, attributes: textAttributes)
FelixSFD
  • 6,052
  • 10
  • 43
  • 117
Rohan Dave
  • 251
  • 1
  • 7