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?

1
  • 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. Commented Jan 4, 2017 at 9:29

8 Answers 8

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!") 
Sign up to request clarification or add additional context in comments.

3 Comments

I'm getting 'DocumentReadingOptionKey' is unavailable in Swift. Have they replaced it with something else since then? Google search turns up nothing.
@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
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.
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.

9 Comments

The setAttributes will reset all the attributes from HTML.
Right, I meant addAttributes. Fixed now. Thanks for catching that.
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 If you have a question, ask a question.
This solution worked perfectly. But I am surprised that for a simple task such a long code was required.
|
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 

2 Comments

Please change "text" in the extension for "html". This will make the answer complete and correct.
@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.
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) } } 

1 Comment

thanks for the quick and exact answer.. I have been searching for this a lot.
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) } } 

1 Comment

you saved my day man plus one for you.
2
let font = "<font face='Montserrat-Regular' size='13' color= 'black'>%@" let html = String(format: font, yourhtmlstring) webView.loadHTMLString(html, baseURL: nil) 

3 Comments

You just add some comments explaining why you do it
it use to change font of webview, you can add any custom font on your webview html string.
It would break markdown like bold, italic fonts
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) } 

Comments

-2
let font = UIFont(name: fontName, size: fontSize) textAttributes[NSFontAttributeName] = font self.attributedText = NSAttributedString(string: self.text, attributes: textAttributes) 

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.