Swift Digest

writing direction 属性

Writing Direction Attribute

Proposal
SF-0022
Authors
Max Obermeier
Review Manager
Tina L
Status
Approved and Implemented

このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら

01 何が問題だったのか

AttributedString には、これまで段落の base writing direction(基本となる書字方向)を 単独の属性として 表現する手段がありませんでした。

UIKit や AppKit には paragraph style というプロパティがあり、その中の一要素として writing direction が含まれていますが、これは NSAttributedString 由来の仕組みで、いくつかの不都合があります。

  • writing direction だけを設定したいときも、paragraph style の他のプロパティの値まで一緒に決めなければなりません。
  • runBoundariesinheritedByAddedText といった AttributedStringKey の高度な振る舞いを活かせません。
  • writing direction は UI フレームワーク固有の話ではなく、双方向テキスト(左から右に書く言語と右から左に書く言語が混在しうるテキスト)を扱うあらゆる文脈で必要になる、文字列の基本的な性質です。

たとえば英語のセンテンスにアラビア語を埋め込む場合と、アラビア語のセンテンスに英語を埋め込む場合とでは、見た目を正しく整えるために必要な base writing direction が異なります。AttributedString 自体にこの情報を持たせられないと、UI フレームワークごとに別の表現で受け渡す必要があり、フレームワークに依存しない処理(双方向テキストの整形やシリアライズなど)でも writing direction を伝搬させづらくなります。

02 どのように解決されるのか

この Proposal は、AttributedString の base writing direction を表す独立した属性を追加します。新しい API は @available(FoundationPreview 6.2, *) でガードされ、FoundationPreview 6.2 以降で利用できます。

AttributedString.WritingDirection

writing direction は、leftToRightrightToLeft の 2 ケースを持つ enum として定義されます。

extension AttributedString {
    @frozen
    public enum WritingDirection: Codable, Hashable, CaseIterable, Sendable {
        case leftToRight
        case rightToLeft
    }
}

ここでの writing direction は「双方向テキストの directional run(同じ方向性を持つ連続した文字の並び)をどちら側から並べていくか」を決める、base direction の指定です。leftToRight なら最初の directional run が左端に置かれ、後続の run が右へ続きます。rightToLeft ではその逆になります。

writing direction は次のいずれとも別の概念です。

  • テキストの alignment(左寄せ・中央寄せなど)
  • line layout direction(行を縦に並べるか横に並べるか)
  • character direction(個々の文字自体の方向性)

ただし alignment の既定値の決定には、しばしば writing direction が利用されます。

縦書きスクリプトで使われる場合、leftToRight は top-to-bottom、rightToLeft は bottom-to-top と解釈されます。これは writing direction が常に line layout direction と直交するように設計されているためです。

双方向テキストを正しく見せるには、テキストの主要言語の Locale.Language.characterDirection に対応する値を writing direction にセットします。たとえばアラビア語が混ざる英文なら .leftToRight、英語が混ざるアラビア文なら .rightToLeft です。

WritingDirectionAttribute キー

属性キー WritingDirectionAttributeAttributeScopes.FoundationAttributes に追加され、writingDirection という名前で参照できます。

extension AttributeScopes.FoundationAttributes {
    public let writingDirection: WritingDirectionAttribute

    @frozen
    public enum WritingDirectionAttribute: CodableAttributedStringKey {
        public typealias Value = AttributedString.WritingDirection
        public static let name: String = "Foundation.WritingDirection"

        public static let runBoundaries: AttributedString
            .AttributeRunBoundaries? = .paragraph

        public static let inheritedByAddedText = false
    }
}

ポイントは 2 つあります。

  • runBoundaries = .paragraph: writing direction は段落単位で意味を持つ性質なので、属性の run も段落境界で切られます。範囲を指定して値を読み書きしても、その範囲と交差する段落 全体 に値が反映されます。
  • inheritedByAddedText = false: ある段落の writing direction は次の段落とは独立に決まるため、段落をまたいで追加されたテキストが既存の writing direction を継承することはありません。

使い方

通常の AttributedString の属性と同じように扱えます。値は optional で、未設定(nil)は「writing direction が指定されていない」状態を表します。

// アラビア文に英単語 "Swift" が埋め込まれているので、全体としては右から左。
var string = AttributedString(
    "Swift مذهل!",
    attributes: .init().writingDirection(.rightToLeft)
)

// writing direction の情報を取り除くには nil を代入する。
string.writingDirection = nil

範囲指定で値を変更しても、runBoundaries = .paragraph の効果でその段落全体に適用されます。

let range = string.range(of: "Swift")!

// "Swift" の範囲だけを指定しても、その段落全体の writing direction が
// .leftToRight に変わる。
string[range].writingDirection = .leftToRight
assert(string.runs[\.writingDirection].count == 1)

同じ段落に文字列を追加すると、既存の writing direction はそのまま新しい部分にも適用されます。一方、改行で新しい段落を始めると、新しい段落は writing direction を継承しません。

// 同じ段落への追加: 既存の writing direction (.leftToRight) が引き継がれる。
string.append(AttributedString(" It is awesome for working with strings!"))
assert(string.runs[\.writingDirection].count == 1)
assert(string.writingDirection == .leftToRight)

// 改行を含む追加: 新しい段落は writing direction を継承しない。
string.append(AttributedString("\nThe new paragraph does not inherit the writing direction."))
assert(string.runs[\.writingDirection].count == 2)
assert(string.runs.last?.writingDirection == nil)

writing direction が必要なのは段落単位の base direction だけで、naturalunknown のようなケースは導入されていません。属性が設定されていない状態(nil)が「writing direction が決まっていない」ことを表すためです。文字列分析などで writing direction を決めたい場合の戦略は、別途自前の属性を用意するか、後述の今後の見通しに従って Foundation 側の API 追加を待つことになります。

03 今後の見通し

双方向テキストを正しく表示するには、テキストの主要言語の character direction に合わせた writing direction を設定する必要があります。Foundation が文字列の strong directionality や Unicode BiDi アルゴリズムの結果をもとに、適切な writing direction を 自動で判定する API を提供することが構想として挙げられています。

なお、ここで述べたのは将来の方向性の示唆であり、その実現を約束するものではありません。文字列分析による自動判定が導入される際には、解析結果として付与した writing direction を文字列の変更に応じて自動的に無効化する仕組み(テキストが変わったときに自動で値をリセットする補助的な属性)も併せて検討される可能性があります。