[Swift] understand TextKit2 piece by piece(NSTextContentManagerDelegate)

     
⌛️ 2 min.
TextKit2 を使っていて気付いたメモ

環境&対象

以下の環境で動作確認を行なっています。

  • macOS14.3
  • Xcode 15.2
  • iOS 17.2
  • Swift 5.9

NSTextContentManager/NSTextContentStorage

TextKi2 の中で、対象の文字列を管理するための class が NSTextContentManager です。
NSTextContentManager は抽象クラスであり、実際に NSTextView/UITextView で TextKit2 で処理される際に使用されている class は、NSTextContentStorage です。backing store として、(デフォルトでは) NSTextStorage を使用しています。

NSTextContentStorage は、NSTextContentManager を継承している class です。


参考
NSTextContentManagerApple Developer Documentation


参考
NSTextContentStorageApple Developer Documentation

classDiagram
class NSTextContentManager
class NSTextContentStorage
NSTextContentManager <|-- NSTextContentStorage

NSTextContentManagerDelegate/NSTextContentStorageDelegate

NSTextContentManager/NSTextContentStorage のいずれも delegate を設定することが可能です。

NSTextContentManagerDelegate で定義されているメソッドは、以下の2つです。

// 指定位置の TextElement を返す
optional func textContentManager(
    _ textContentManager: NSTextContentManager,
    textElementAt location: NSTextLocation
) -> NSTextElement?
// 渡された TextElement の処理をスキップすべきか返す
optional func textContentManager(
    _ textContentManager: NSTextContentManager,
    shouldEnumerate textElement: NSTextElement,
    options: NSTextContentManager.EnumerationOptions = []
) -> Bool

NSTextContentStorage で定義されているメソッドは、以下の1つです。

// 指定 range の TextParagraph を返す
optional func textContentStorage(
    _ textContentStorage: NSTextContentStorage,
    textParagraphWith range: NSRange
) -> NSTextParagraph?

optional 指定されていることからもわかりますが、いずれのメソッドも実装しなくて構いません。

似た目的の Delegate

以下の2つの メソッドの目的が近いことに気づきます。

// NSTextContentManagerDelegate
// 指定位置の TextElement を返す
optional func textContentManager(
    _ textContentManager: NSTextContentManager,
    textElementAt location: NSTextLocation
) -> NSTextElement?
// NSTextContentStorageDelegate
// 指定 range の TextParagraph を返す
optional func textContentStorage(
    _ textContentStorage: NSTextContentStorage,
    textParagraphWith range: NSRange
) -> NSTextParagraph?

誰が 返すTextElement/TextParagraph が採用されるのか?

例えば、2つのメソッドの両方とも NSTextElement/NSTextParagraph を返してしまうとどうなるでしょうか?

この2つの関係性を調べてみました。

以下では、NSTextContentManagerDelegate.textContentManager(_:,textElementAt:) を “A” と
NSTextContentStorageDelegate.textContentStorage(_:,textParagraphWith:) を “B” と呼んでいます。

・A, B 2つとも実装して、どちらも nil を返す
・A, B 2つとも実装して、どちらもそれぞれ NSTextParagraph を返す
・A, B 2つとも実装するが Aは nil を返し、B は NSTextParagraph を返す
・A, B 2つとも実装するが Aは NSTextParagraph を返し、B は nil を返す

確認に使用したコードは以下です。必要に応じて、コメント化/コメント化解除 を切り替えて試しました。
なお、ログに現れてくる 10 という数値は、NSTextStorage に10文字セットしているためです。

extension TextEditViewModel: NSTextContentStorageDelegate {
    public func textContentManager(_ textContentManager: NSTextContentManager, textElementAt location: NSTextLocation) -> NSTextElement? {
        OSLog.textEditViewModelLogger.debug("_:textElementAt: with location \(location.description)")

        return nil
        
//        guard let textStorageManager = textContentManager as? NSTextContentStorage else { return nil }
//        guard let docAttrString = textStorageManager.attributedString else { return nil }
//        let textParagraph = NSTextParagraph(attributedString: docAttrString)
//        return textParagraph
    }

    public func textContentStorage(_ textContentStorage: NSTextContentStorage, textParagraphWith range: NSRange) -> NSTextParagraph? {
        OSLog.textEditViewModelLogger.debug("_:textParagraphWith: with range:\(range)")
        return nil
        
//        guard let docAttrString = textContentStorage.attributedString else { return nil }
//        let textParagraph = NSTextParagraph(attributedString: docAttrString)
//        return textParagraph
    }
}

A,B どちらも nil を返す

以下が実行したときのログです。

_:textElementAt: with location 0
_:textParagraphWith: with range:{0, 10}

ログから見ると、どちらも呼び出されています。 A, B どちらも呼び出されていますが、A の方が先に呼び出されているようです。

A, B 2つとも実装して、どちらもそれぞれ NSTextParagraph を返す

以下が実行したときのログです。

_:textElementAt: with location 0

ログから見ると、A は呼び出されていますが、B は呼び出されていません。

A, B 2つも実装するが Aは nil を返し、B は NSTextParagraph を返す

以下が実行したときのログです。

_:textElementAt: with location 0
_:textParagraphWith: with range:{0, 10}

ログから見ると、A が呼び出された後、B も呼び出されています。

A, B 2つも実装するが Aは NSTextParagraph を返し、B は nil を返す

以下が実行したときのログです。

_:textElementAt: with location 0

ログから見ると、A が呼び出され、その後 B は呼び出されません。

考察

つまり、以下のような流れのようです。
1) NSTextContentManagerDelegate.textContentManager(_:,textElementAt:) 側から試す
2-1) NSTextElement が返されれば採用する
2-2) nil が返された場合は、(可能であれば) NSTextContentStorageDelegate.textContentStorage(_:,textParagraphWith:) を試す

まとめ

NSTextContentManager/NSTextContentStorage の Delegate による NSTextElement/NSTextParagraph の生成フロー

NSTextContentManager/NSTextContentStorage の Delegate による NSTextElement/NSTextParagraph の生成フロー
  • NSTextContentManagerDelegate.textContentManager(_:,textElementAt:) が試される
  • NSTextContentStorageDelegate.textContentStorage(_:,textParagraphWith:) は、NSTExtContentManagerDelegate での試行の後に 試される
  • as of macOS Sonoma 14.3
  • Apple のドキュメントには記載はありませんので バージョンによって変わるかもしれません

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

SwiftUI おすすめ本

SwiftUI を理解するには、以下の本がおすすめです。

SwiftUI ViewMatery

SwiftUI で開発していくときに、ViewやLayoutのための適切なmodifierを探すのが大変です。
英語での説明になってしまいますが、以下の”SwiftUI Views Mastery Bundle”という本がビジュアル的に確認して探せるので、便利です。

英語ではありますが、1ページに コードと画面が並んでいるので、非常にわかりやすいです。

View に適用できる modifier もわかりやすく説明されているので、ビューの理解だけではなく、どのような装飾ができるかも簡単にわかります。

超便利です

SwiftUIViewsMastery

販売元のページは、こちらです。

SwiftUI 徹底入門

# SwiftUI は、毎年大きく改善されていますので、少し古くなってしまいましたが、いまでも 定番本です。

Swift学習におすすめの本

詳解Swift

Swift の学習には、詳解 Swift という書籍が、おすすめです。

著者は、Swift の初期から書籍を出していますし、Swift の前に主力言語だった Objective-C という言語についても同様の書籍を出しています。

最新版を購入するのがおすすめです。

現時点では、上記の Swift 5 に対応した第5版が最新版です。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です