Sponsor Link
環境&対象
- 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 の生成フロー
- 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 もわかりやすく説明されているので、ビューの理解だけではなく、どのような装飾ができるかも簡単にわかります。
超便利です
販売元のページは、こちらです。
SwiftUI 徹底入門
# SwiftUI は、毎年大きく改善されていますので、少し古くなってしまいましたが、いまでも 定番本です。
Swift学習におすすめの本
詳解Swift
Swift の学習には、詳解 Swift という書籍が、おすすめです。
著者は、Swift の初期から書籍を出していますし、Swift の前に主力言語だった Objective-C という言語についても同様の書籍を出しています。
最新版を購入するのがおすすめです。
現時点では、上記の Swift 5 に対応した第5版が最新版です。
Sponsor Link