AttributeContainer のフィルタリング
AttributeContainer Filtering
このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら↗。
01 何が問題だったのか
AttributedString の各属性は、AttributedStringKey の宣言を通して「属性自身の振る舞い」を持っています。たとえば、段落や文字といった単位の境界に値が揃うように振る舞う属性、ほかの属性やテキストの変更によって無効化される属性、末尾にテキストを追記したときに引き継がれない属性、といったものです。これらの振る舞いは個々のキー型の側で宣言されます。
ところが、いったん値が AttributedString や AttributeContainer に格納されてしまうと、そこに入っている属性がどのような振る舞いを持つキーに由来するのか、利用側からは動的に区別できませんでした。この区別が必要になるのが、AttributedString をテキストエディタなどのリッチテキスト UI のモデルとして使う場合です。
たとえば「タイピング属性」(カーソル位置の属性で、これから入力される文字に適用されるもの)を取り出したいときには、追記されるテキストに引き継がれる属性だけに絞り込みたくなります。あるいは、ある位置に付いている段落単位の属性だけを別途扱いたい、という要件もあります。こうしたフィルタリングは Foundation の外で書こうとすると、結局 AttributeContainer から各属性キーの振る舞いを引き出す手段がないため、Foundation の外側にある高レベル API では実現が難しい状態でした。
02 どのように解決されるのか
AttributeContainer に、属性キーの振る舞いに基づいて中身を絞り込むための filter(...) API が追加されます。FoundationPreview 6.2 以降で利用できます。
@available(FoundationPreview 6.2, *)
extension AttributeContainer {
public func filter(inheritedByAddedText: Bool) -> AttributeContainer
public func filter(runBoundaries: AttributedString.AttributeRunBoundaries?) -> AttributeContainer
}
それぞれのオーバーロードは、もとの AttributeContainer の中から、指定した条件にマッチする属性キーに由来する値だけを取り出した新しい AttributeContainer を返します。
追記されるテキストに引き継がれる属性で絞る
filter(inheritedByAddedText:) は、属性キーが宣言する inheritedByAddedText プロパティが指定値と一致する属性だけを残します。テキストエディタでカーソル位置の「タイピング属性」を取り出すケースが典型例です。
func typingAttributes(in text: AttributedString, selection: UserTextSelection) -> AttributeContainer {
if selection.isSingleCursor {
return text.runs[selection.index].attributes.filter(inheritedByAddedText: true)
} else {
return text.runs[selection.startIndex].attributes.filter(inheritedByAddedText: true)
}
}
ここでは AttributedString.Runs.Run の .attributes で run 内の属性をまとめた AttributeContainer を取り出し、追記されるテキストに引き継がれる属性だけに絞り込んでいます。逆に false を渡せば、追記時に引き継がれない属性だけを取り出すこともできます。なお、UserTextSelection は説明のための仮の型で、Foundation の API ではありません。
run boundary で絞る
filter(runBoundaries:) は、属性キーが宣言する run boundary(属性が値を揃える単位)に基づくフィルタです。たとえば、段落単位で値が揃う属性だけを取り出したい場合は次のように書きます。
let paragraphAttributes = container.filter(runBoundaries: .paragraph)
引数に nil を渡したときは、特定の run boundary に紐付かない属性だけが返ります。run boundary を持たない属性と特定の boundary を持つ属性を一度に同じ呼び出しで扱うのではなく、nil の場合と特定値の場合とで明確に分けられている点がポイントです。
1 引数ずつのオーバーロードという形
filter は条件ごとに別々のオーバーロードとして提供され、複数条件を 1 度に指定する形にはなっていません。これは、引数なしで呼ぶと no-op になってしまうことや、runBoundaries の nil の意味が曖昧になることを避けるためで、複数条件を組み合わせたい場合は filter を続けて呼びます。
03 今後の見通し
この提案では具体的な今後の構想は示されていませんが、将来 AttributedStringKey に新たな振る舞いが追加されたときには、それに対応する filter(...) のオーバーロードを AttributeContainer に追加していくことが想定されています。
なお、ここで述べたのは将来の方向性の示唆であり、その実現を約束するものではありません。実際の追加は別の Proposal として議論されることになります。