在 iOS 中将 HTML 转换为 NSAttributedString

我使用 UIWebView的一个实例来处理一些文本并正确地给它上色,它给出的结果是 HTML,而不是显示在 UIWebView中,我想使用 Core TextNSAttributedString来显示它。

我能够创建和绘制的 NSAttributedString,但我不确定如何我可以转换和映射到属性字符串的 HTML。

我知道在 Mac OS X NSAttributedString下有一个 initWithHTML:方法,但是这只是 Mac 的附加功能,不适用于 iOS。

我也知道有一个类似的问题,但它没有答案,我想我会再次尝试,看看是否有人创造了这样做的方式,如果是这样,如果他们可以分享它。

123882 次浏览

现在唯一的解决方案是解析 HTML,构建一些具有给定 point/font/etc 属性的节点,然后将它们组合成一个 NSAttributedString。工作量很大,但如果正确完成,将来可以重用。

Github 的 Oliver Drobnik 正在开发一个 对 NSAttributedString 的开放源码添加,它使用 NSScanner 进行 HTML 解析。

在 iOS7中,UIKit 添加了一个 initWithData:options:documentAttributes:error:方法,它可以使用 HTML 初始化 NSAttributedString,例如:

[[NSAttributedString alloc] initWithData:[htmlString dataUsingEncoding:NSUTF8StringEncoding]
options:@{NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType,
NSCharacterEncodingDocumentAttribute: @(NSUTF8StringEncoding)}
documentAttributes:nil error:nil];

斯威夫特:

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)

从 HTML 创建 NSAttributedString 必须在主线程上完成!

更新: 原来 NSAttributedStringHTML 呈现依赖于底层的 WebKit 和 必须在主线程上运行或者它偶尔会崩溃的应用程序与 SIGTRAP

返回文章页面新文物崩溃日志:

enter image description here

下面是更新后的 线程安全 Swift 2 String 扩展:

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)
}
}
}
}

用法:

let html = "<center>Here is some <b>HTML</b></center>"
html.attributedStringFromHTML { attString in
self.bodyLabel.attributedText = attString
}

产出:

enter image description here

上述解答是正确的。

[[NSAttributedString alloc] initWithData:[htmlString dataUsingEncoding:NSUTF8StringEncoding]
options:@{NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType,
NSCharacterEncodingDocumentAttribute: @(NSUTF8StringEncoding)}
documentAttributes:nil error:nil];

但是如果你在 ios 8.1,2或3上运行它,这个应用程序就会崩溃。

为了避免崩溃,您可以这样做: 在队列中运行它。所以它总是在主线上。

这是一个用 Swift 编写的 String扩展,它返回一个 HTML 字符串作为 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
}
}

使用,

label.attributedText = "<b>Hello</b> \u{2022} babe".htmlAttributedString()

在上面的代码中,我特意添加了一个 unicode u2022,以显示它正确地呈现 unicode。

一个小问题: NSAttributedString使用的默认编码是 NSUTF16StringEncoding(而不是 UTF8!)。

有用的扩展

受到这个线程、一个 pod 和 Erica Sadun 在 iOS Gourmet Cookbook p. 80中的 Object 示例的启发,我在 StringNSAttributedString上编写了一个扩展,在 HTML 普通字符串和 NSAttributedStrings 之间来回切换,反之亦然——在 GitHub 给你上,我发现这很有帮助。

签名是(同样,Gist 中的完整代码,链接在上面) :

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 }

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
}

安德鲁的解决方案进行了一些修改,并将代码更新为 Swift 3:

此代码现在使用 UITextView 作为 self,并能够继承其原始字体、字体大小和文本颜色

注意: toHexString()给你的扩展

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)
}
}
}
}

示例用法:

mainTextView.setAttributedStringFromHTML("<i>Hello world!</i>") { _ in }

NSHTMLTextDocumentType 的使用速度很慢,而且很难控制样式。我建议你试试我的图书馆叫 Atributika。它有自己的非常快的 HTML 解析器。您还可以拥有任何标记名称,并为它们定义任何样式。

例如:

let str = "<strong>Hello</strong> World!".style(tags:
Style("strong").font(.boldSystemFont(ofSize: 15))).attributedString


label.attributedText = str

你可以在这里找到它

斯威夫特3:
试试这个 :

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
}
}

使用方法:

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()

NSAttributedString 上的快速初始值设定项扩展

我倾向于把它作为 NSAttributedString而不是 String的扩展来添加。我尝试将它作为静态扩展和初始化程序。我更喜欢下面所包含的初始化程序。

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)
}
}

例子

let html = "<b>Hello World!</b>"
let attributedString = NSAttributedString(html: html)

Swift 4


  • 方便的初始值设定项
  • 没有额外的守卫
  • 抛出错误

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)
}


}

用法

UILabel.attributedText = try? NSAttributedString(htmlString: "<strong>Hello</strong> World!")

尊重字体家族,动态字体我炮制了这个令人厌恶的东西:

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)
}


}

或者,您可以使用这个版本的派生和设置 在 UILabel 上设置 AttributedString 之后的 font

但是,这会使封装在属性字符串中的大小和大胆性大打折扣

把所有的答案都读到这里真是太棒了。 你是一个非常有耐心的男人,女人或孩子。

内置的转换总是将文本颜色设置为 UIColor.black,即使您使用。For ground Color 设置为其他值。要在 iOS13上支持 DARK 模式,请在 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)
}
}

以下是 Swift 5版本的 莫比尔 · 丹的回答:

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)
}
}

将 html 转换为 AttributedNSAttributedString 的函数,该函数将适应动态大小 + 适应文本的可访问性。

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
    

}

使用方法:

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

添加此扩展,然后使用文本。使用此代码后,我们可以使用 自定义文本大小。

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
}
}
}