【问题标题】:Convert HTML to NSAttributedString in iOS在 iOS 中将 HTML 转换为 NSAttributedString
【发布时间】:2025-12-17 23:05:01
【问题描述】:

我正在使用UIWebView 的实例来处理一些文本并正确着色,它以HTML 形式给出结果,但不是在UIWebView 中显示它我想使用Core Text 和@987654325 显示它@。

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

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

我也知道有一个与此类似的问题,但没有答案,但我会再试一次,看看是否有人创造了一种方法来做到这一点,如果有,他们是否可以分享。

【问题讨论】:

  • NSAttributedString-Additions-for-HTML 库已被同一作者重命名并整合到一个框架中。它现在被称为 DTCoreText 并且包括一堆核心文本布局类。您可以通过here 找到它

标签: iphone objective-c cocoa-touch core-text nsattributedstring


【解决方案1】:

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

【讨论】:

  • 由于某种原因,选项 NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType 导致编码需要非常非常长的时间:(
  • 太糟糕了 NSHTMLTextDocumentType (字面上)比使用 NSRange 设置属性慢约 1000 倍。 (用一个粗体标记描述一个短标签。)
  • 请注意,如果您想从后台线程中使用此方法,则无法使用此方法 NSHTMLTextDocumentType。即使使用 ios 7,它也不会使用 TextKit 进行 HTML 渲染。看看 Ingve 推荐的 DTCoreText 库。
  • 太棒了。只是一个想法,您可能可以将 [NSNumber numberWithInt:NSUTF8StringEncoding] 设为 @(NSUTF8StringEncoding),不是吗?
  • 我正在这样做,但在 iOS 8 上要小心。它非常缓慢,几百个字符接近一秒。 (在 iOS 7 中,它几乎是瞬间完成的。)
【解决方案2】:

Github 上的 Oliver Drobnik 正在开发 open source addition to NSAttributedString。它使用 NSScanner 进行 HTML 解析。

【讨论】:

  • 需要最少部署 iOS 4.3 :( 不过,非常令人印象深刻。
  • @Lirik Overkill 可能对你来说是完美的,但对其他人来说是完美的,即你的评论一点帮助都没有。
  • 请注意,这个项目需要是开源的,并且包含在标准的 2-clause BSD 许可证中。这意味着您必须提及 Cocoanetics 作为此代码的原始作者,并在您的应用程序中重现 LICENSE 文本。
【解决方案3】:

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

更新:事实证明 NSAttributedString HTML 渲染依赖于 WebKit,并且必须在主线程上运行 否则它会偶尔使用 SIGTRAP 使应用程序崩溃.

New Relic 崩溃日志:

以下是更新后的 线程安全 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 中可用的日历事件、电话、电子邮件、网站链接等吗?与 UILabel 相比,我希望 UITextView 能够处理事件。
  • 上述方法只对格式化有用。如果您需要事件处理,我建议您使用TTTAttributedLabel
  • NSAttributedString 使用的默认编码是 NSUTF16StringEncoding(不是 UTF8!)。这就是为什么这行不通。至少在我的情况下!
  • 这应该是公认的解决方案。在后台线程上进行 HTML 字符串对话最终崩溃,并且在运行测试时非常频繁。
【解决方案4】:

NSAttributedString 上的 Swift 初始化扩展

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

斯威夫特 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)
}

斯威夫特 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 变成这样

    hello world

  • 保存一些 LOC 并将 guard ... NSMutableAttributedString(data:... 替换为 try self.init(data:...(并将 throws 添加到 init 中)
  • 最后它不起作用 - 文本获得随机字体大小
  • 您正在使用 UTF-8 解码数据,但您使用 UTF-16 对其进行编码
【解决方案5】:

这是一个用 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!
  • UTF16 拯救了我的一天,谢谢 samwize!
【解决方案6】:

Andrew的解决方案做了一些修改,并将代码更新为Swift 3:

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

注意:toHexString()here 的扩展

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 }

【讨论】:

    【解决方案7】:

    Swift 3.0 Xcode 8 版本

    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
    }
    

    【讨论】:

      【解决方案8】:

      斯威夫特 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 meta.stackexchange.com/questions/5234/…,点赞 :) 谢谢!
      • 如何设置字体大小和字体系列?
      • 这比 Mobile Dan 建议的要好得多,因为它不涉及 self.init(attributedString: attributesString) 的冗余副本
      【解决方案9】:

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

      【讨论】:

      • 如果 HTML 是 XHTML-Strict,你可以使用 NSXMLDOcument 和朋友来帮助解析。
      • 您建议我如何构建具有给定属性的节点?
      • 这是一个实现细节。无论您如何解析 HTML,您都可以访问每个标签的每个属性,这些属性指定字体名称、大小等内容。您可以使用此信息来存储您需要添加到属性文本中的相关详细信息作为属性.通常,在处理此类任务之前,您需要先熟悉解析。
      【解决方案10】:

      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

      【讨论】:

        【解决方案11】:

        上述解决方案是正确的。

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

        但如果您在 ios 8.1,2 或 3 上运行该应用程序会崩溃。

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

        【讨论】:

        • @alecex 我确实遇到了同样的问题!应用程序将在 iOS 8.1、2、3 上崩溃。但在 iOS 8.4 或更高版本上会很好。你能详细解释一下如何避免吗?或者有什么解决方法,或者可以使用方法来代替?
        • 我做了一个快速分类来处理这个问题,从 AppKit 复制方法,它有一个非常简单和直观的方法来做到这一点。为什么 Apple 没有添加它超出了我的理解。:github.com/cguess/NSMutableAttributedString-HTML
        【解决方案12】:

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

        【讨论】:

          【解决方案13】:

          内置的转换总是将文本颜色设置为 UIColor.black,即使您传递一个属性字典并将 .forgroundColor 设置为其他值。要在 iOS 13 上支持 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)
              }
          }
          

          【讨论】:

            【解决方案14】:

            这是Swift 5Mobile Dan's answer 版本:

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

            【讨论】:

            • 如何在 SwiftUI 视图中实现这一点?
            【解决方案15】:

            有用的扩展

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

            签名是(同样,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 }
            

            【讨论】:

              【解决方案16】:

              尊重字体系列,动态字体我炮制了这个可憎的:

              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://*.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 上的字体

              这将破坏封装在属性字符串中的大小和粗体

              感谢您阅读到这里的所有答案。 你是一个非常有耐心的男人女人或孩子。

              【讨论】: