TextKit 2 - 如何创建单词级 NSTextElements 以标记文本中的单词

Pet*_*r R 6 textkit swift

我正在尝试创建标签/标签/徽章来标记某些单词(实际上有时多个单词可以构成一个标签)。目前,我添加了一个自定义 NSAttributedString 键,用于用词性标记单词。我想用圆形背景色标记这些单词。我已经浏览了此示例代码中的代码,它创建了如下标签:

TextKit2 段落元素

但示例代码仅采用现有的 NSTextElements(段落级别)并对它们应用样式。当我从文本代码插入相同的委托函数时,所有文本元素都是段落级别的。例如

This is one.
This is two.
Run Code Online (Sandbox Code Playgroud)

将创建两个文本元素:This is one.This is two.。我很好奇如何将它们分成多个文本元素。例如,如果我只想标记EGGin EGG SANDWICH NO. 2,我猜我需要将该文本元素分成两个(EGG 和 SANDWICH NO.2),但我目前不知道如何做到这一点,或者找到任何示例在MacOS上为文本添加文字标签(该应用程序不需要支持iOS,只支持MacOS)。

我是否走在实现这一目标的正确道路上?如果是这样,拆分 NSTextElements 的最佳方法是什么?

M W*_*ilm 4

我遇到了类似的问题,并且以一种我真的不鼓励这样做的方式失败了。

TextKit 2 正在不断发展。我报告了尝试在 macOS Ventana 中创建低于 NSTextParagraph 级别的 NSTextElement 子类(2022 年秋季)。

我的问题是将作为属性字符串一部分的文本附件的布局位置传达给文本视图,以相应地定位关联的子视图及其排除路径 - 这是带有数字的文本的标准问题。

将 NSTextElement 的子类引入文本布局过程的路径是通过委托。

从文本存储开始: 提供根据以下内容NSTextContentStoreDelegate引入自定义的方法:NSTextElementsNSAttributedString

1. 段落级别:

func textContentStorage(NSTextContentStorage, textParagraphWith: NSRange) -> NSTextParagraph? 
Run Code Online (Sandbox Code Playgroud)

2.任意NSTextElement级别:

func textContentManager(NSTextContentManager, textElementAt: NSTextLocation) -> NSTextElement?
Run Code Online (Sandbox Code Playgroud)

NSTextLayoutManagerDelegate可以NSTextLayoutFragment为这些自定义提供自定义对象NSTextElements

 func textLayoutManager( _ textLayoutManager: NSTextLayoutManager, textLayoutFragmentFor location: NSTextLocation, in textElement: NSTextElement) -> NSTextLayoutFragment
Run Code Online (Sandbox Code Playgroud)

自定义 NSTextLayoutFragment 对象可以执行您想要的操作。

我尝试在NSTextElement级别上进行子类化(使用 的方法 2 NSTextContentStoreDelegate)并使用自定义NSTextLayoutFragment来执行我的代码。结果是,当NSTextLayoutFragment遇到这个自定义对象时,整个布局过程就停止了。我重写了文档中的所有方法来NSTextLayoutFragment捕获问题,但失败了。更糟糕的是,我在代码中引入了 Swift 作为一种语言应该防范的内存问题。在我和编译器期望模型对象的地方,执行代码看到显示该模型对象的视图。

我最终接受了我不能低于段落级别并NSTextParagraph使用第一种方法而不是第二种NSTextContentStorageDelegate方法进行子类化和引入此类。我将所需的雾化问题解决到了子类中的附件。这立即发挥了作用,并以非常精确和高效的方式带来了所需的功能。

这是我使用的 NSTextParagraph 子类:

class ParItemAttachmentTextParagraph: NSTextParagraph {
   
   var attachmentRanges: Array<NSRange>
 
   init(attributedString:NSAttributedString, textContentManager: NSTextContentManager, elementRange: NSTextRange?, attachmentRanges ranges:Array<NSRange>) {
      attachmentRanges = ranges
      super.init(attributedString: attributedString)
      self.textContentManager = textContentManager
      self.elementRange = elementRange
   }
   
}
Run Code Online (Sandbox Code Playgroud)

该类由 NSTextContentStorageDelegate 引入到布局过程中:

class TextContentStorageDelegate: NSObject, NSTextContentStorageDelegate {
   
   var attachmentRanges: Array<NSRange> = []
   
   // Returning a ParItemAttachmentTextElement if there is a ParagraphItemAttachment in the text range
   func textContentStorage(_ textContentStorage: NSTextContentStorage, textParagraphWith range: NSRange) -> NSTextParagraph? {
      if let attributedString = textContentStorage.attributedString {
         self.attachmentRanges = Array()
         attributedString.enumerateAttribute(.attachment, in: range) { (attachment: Any?, characterRange:NSRange, stopIt: UnsafeMutablePointer<ObjCBool>) in
            if let _ = attachment as? ParagraphItemAttachment2 {
               self.attachmentRanges.append(characterRange)
            }
         }
         if !self.attachmentRanges.isEmpty {
            let textElementAttributedString = attributedString.attributedSubstring(from: range)
            let textRange = textContentStorage.textRange(from: range)
            let parItemTextElement = ParItemAttachmentTextParagraph(attributedString: textElementAttributedString, textContentManager: textContentStorage, elementRange: textRange, attachmentRanges: self.attachmentRanges)
            return parItemTextElement
         }
      }
      return nil
   }
Run Code Online (Sandbox Code Playgroud)

NSTextLayoutManager 通过 ist NSTextLayoutManagerDelegate 对此类段落的检测做出反应:

class TextLayoutManagerDelegate: NSObject, NSTextLayoutManagerDelegate {
   
   func textLayoutManager( _ textLayoutManager: NSTextLayoutManager, textLayoutFragmentFor location: NSTextLocation, in textElement: NSTextElement) -> NSTextLayoutFragment {
      
      if let parItemTextElement = textElement as? ParItemAttachmentTextParagraph {
         return ParItemAttachmentLayoutFragment(textElement: parItemTextElement, range: parItemTextElement.elementRange)
      }
      return NSTextLayoutFragment(textElement: textElement, range: textElement.elementRange)
   }
   
}
Run Code Online (Sandbox Code Playgroud)

并引入了一个特殊的NSTextLayoutFragment——ParItemAttachmentLayoutFragment

这是 ParItemAttachmentLayoutFragment:

class ParItemAttachmentLayoutFragment: NSTextLayoutFragment {
   
   var frameChangeSubscriber : Cancellable? = nil
   
   init(textElement:ParItemAttachmentTextParagraph, range:NSTextRange?) {
      super.init(textElement: textElement, range: range)
      
      let frameChangePublisher = self.publisher(for: \.layoutFragmentFrame, options: [.new])
      
      let frameChangeSubscriber = frameChangePublisher.throttle(for: .seconds(1), scheduler: RunLoop.main, latest: true).sink(receiveValue: {[weak self] newFrame in
         if let layoutFragment = self {
            Task { @MainActor () -> Void in
               layoutFragment.textViewController?.updateSubiewLocations(layoutFragment.paragraphItemPersistentIDs, layoutFragment: layoutFragment)
            }
         }
      })
      self.frameChangeSubscriber = frameChangeSubscriber
      
   }
   
   required init?(coder: NSCoder) {
      super.init(coder: coder)
   }
   
   deinit {
      self.frameChangeSubscriber?.cancel()
   }

   
   var textView: ParagraphNSView? {
      get {
         return self.textLayoutManager?.textContainer?.textView as? ParagraphNSView
      }
   }
   
   var textViewController: ParagraphNSViewController? {
      get {
         return self.textView?.viewController
      }
   }
   
   var paragraphItemAttachments: Array<ParagraphItemAttachment2> {
      get {
         var parItemAttachments: Array<ParagraphItemAttachment2> = Array()
         if let textElement = self.textElement as? ParItemAttachmentTextParagraph {
            let attributedString = textElement.attributedString
            let range = NSRange(location: 0, length: attributedString.length)
            attributedString.enumerateAttribute(.attachment, in: range) { (attachment: Any?, characterRange:NSRange, stopIt: UnsafeMutablePointer<ObjCBool>) in
               if let parItemAttachment = attachment as? ParagraphItemAttachment2 {
                  parItemAttachments.append(parItemAttachment)
               }
            }
         }
         return parItemAttachments
      }
   }
   
   var paragraphItemPersistentIDs: Array<PersistentIdentifier> {
      get {
         let parItemAttachments = self.paragraphItemAttachments
         let theParItems = parItemAttachments.compactMap({
            return $0.persistentIdentifier
         })
         return theParItems
      }
   }

   
}
Run Code Online (Sandbox Code Playgroud)

一旦布局片段更改其框架,此布局片段就会调用一个方法来重新定位其关联的子视图:

updateSubiewLocations(layoutFragment.paragraphItemPersistentIDs, layoutFragment: layoutFragment)
Run Code Online (Sandbox Code Playgroud)

你看,这很复杂,而且确实嵌入到我的用例中。很难直接使用 - 但你明白了。