在iOS中将HTML转换为NSAttributedString

165

我正在使用一个 UIWebView 实例来处理一些文本并将其正确着色,它以HTML的形式呈现结果,但我不想在 UIWebView 中显示它,而是想使用 NSAttributedString 使用 Core Text 来显示它。

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

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

我也知道有类似的问题,但没有答案,所以我想再试一次,看看是否有人已经创建了一种方法来解决这个问题,如果有,是否可以分享一下。


2
NSAttributedString-Additions-for-HTML 库已由同一作者更名并整合为一个框架。现在它被称为 DTCoreText 并包含了一系列 Core Text 布局类。你可以在这里找到它。 - Brian Douglas Moakley
18个回答

310
在iOS 7中,UIKit新增了一个initWithData:options:documentAttributes:error:方法,可以使用HTML初始化一个NSAttributedString对象,例如:
[[NSAttributedString alloc] initWithData:[htmlString dataUsingEncoding:NSUTF8StringEncoding] 
                                 options:@{NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType,
                                           NSCharacterEncodingDocumentAttribute: @(NSUTF8StringEncoding)} 
                      documentAttributes:nil error:nil];

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

31
由于某种原因,选项NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType导致编码时间非常长 :( - Arie Litovsky
17
很遗憾,NSHTMLTextDocumentType 的速度(实际上)比使用 NSRange 设置属性要慢大约 1000 倍。(对一个带有一个粗体标签的简短标签进行了剖析。) - Jason Moore
6
请注意,如果你想从后台线程使用该方法,则不能使用NSHTMLTextDocumentType。即使在iOS 7上,它也不会使用TextKit来进行HTML渲染。建议查看Ingve推荐的DTCoreText库。 - TJez
2
太棒了。不过,你可能可以将[NSNumber numberWithInt:NSUTF8StringEncoding]写成@(NSUTF8StringEncoding),对吧? - Jarsen
17
我正在进行这个操作,但是在 iOS 8 上需要小心,速度非常慢,对于几百个字符接近一秒钟的时间。(在 iOS 7 上几乎是瞬间完成的。) - Norman
显示剩余20条评论

47

在Github上,Oliver Drobnik正在进行一个名为DTCoreText的开源项目,该项目正在完善中。它使用NSScanner来解析HTML。


需要最低部署iOS 4.3 :( 尽管如此,非常令人印象深刻。 - Oh Danny Boy
3
对于你来说可能过度了,但对于其他人来说可能正好合适。也就是说,你的评论根本没有帮助。 - wuf810
3
请注意,该项目是开放源代码,并由标准的2条款BSD许可证保护。这意味着您必须在应用程序中提及Cocoanetics作为该代码的原始作者,并重现许可证文本。 - dulgan

35

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

更新:事实证明,NSAttributedString HTML渲染取决于底层的WebKit,必须在主线程上运行否则会偶尔导致应用程序崩溃并显示SIGTRAP。

New Relic崩溃日志:

enter image description here

下面是一个已更新的线程安全的Swift 2字符串扩展:

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
}

输出:

在此输入图像描述


安德鲁。这个工作正常。我想知道如果我采用这种方法,我必须处理哪些类型的事件在我的UITextView中。它能够处理HTML中可用的日历事件、电话、电子邮件、网站链接等吗?我希望UITextView能够比UILabel处理事件。 - harshit2811
上述方法仅适用于格式设置。如果需要处理事件,我建议使用TTTAttributedLabel - Andrew Schreiber
NSAttributedString默认使用的编码是NSUTF16StringEncoding(不是UTF8!)。这就是为什么它不起作用。至少在我的情况下是这样! - Umit Kaya
这应该是被接受的解决方案。在后台线程上进行HTML字符串转换 最终崩溃,并且在运行测试时经常发生。 - ratsimihah

23

NSAttributedString 的 Swift 初始化器扩展

我倾向于将其作为 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)

我想让“Hello World”变成这样:<p><b><i>hello</i></b> <i>world</i></p>。 - Uma Madhavi
节省一些代码行数,将 guard ... NSMutableAttributedString(data:... 替换为 try self.init(data:...(并在 init 中添加 throws)。 - nyg
最后它不起作用了 - 文本获得随机字体大小。 - Vyachaslav Gerchicov
2
你正在使用UTF-8解码数据,但你却使用UTF-16进行编码。 - Shyam Bhat

12

这是一个用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!)。


UTF16 挽救了我的一天,谢谢 samwize! - Yueyu
UTF16 保住了我的一天,感谢 samwize! - Yueyu

6

Swift 4


  • NSAttributedString方便初始化器
  • 无需额外保护
  • 抛出错误

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

你救了我的一天。谢谢。 - pkc456
@pkc456 https://meta.stackexchange.com/questions/5234/how-does-accepting-an-answer-work,请点赞 :) 谢谢! - AamirR
我该如何设置字体大小和字体族? - kirqe
这比Mobile Dan建议的要好得多,因为它不涉及使用self.init(attributedString: attributedString)进行冗余复制。 - cyanide

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
}

6

安德鲁的解决方案上进行了一些修改并将代码更新为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 }

4

使用NSHTMLTextDocumentType会很慢,而且很难控制样式。我建议您尝试我的库,名为Atributika。它有自己的非常快速的HTML解析器。此外,您可以拥有任何标签名称并为它们定义任何样式。

示例:

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

label.attributedText = str

你可以在这里找到它 https://github.com/psharanda/Atributika

3

你现在唯一的解决方案是解析HTML,根据给定的点/字体等属性建立一些节点,然后将它们组合成NSAttributedString。这是很多工作,但如果做得正确,以后可以重复使用。


1
如果HTML是XHTML-Strict,您可以使用NSXMLDOcument和相关工具来帮助解析。 - Dylan Lukes
你会建议我如何构建具有给定属性的节点? - Joshua
2
这是一个实现细节。无论你如何解析HTML,你都可以访问每个标签的每个属性,这些属性指定了诸如字体名称、大小等内容。你可以使用这些信息将相关细节作为属性存储,以便添加到属性文本中。通常,在处理此类任务之前,您需要先熟悉解析技术。 - jer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接