WWDC25:Code-along: Cook up a rich text experience in SwiftUI with AttributedString

AttributedString について知りたいと思ったら、TextEditor に関する内容だった。新しい TextEditor を使う機会が現状ないのと、装飾文字列を駆使する機会も今のところないが、文字列処理におけるスライスという言葉を知れて良かった。


0:00 – Introduction

  • AttributedString を使ったリッチテキスト編集体験の構築
  • TextEditorAttributedString 対応にアップグレードし、カスタムコントロールを構築、独自のテキスト書式定義を作成

1:15 – TextEditor and AttributedString

  • TextEditorAttributedString 対応
    • TextEditor(text:) に渡す @Binding var text の型を StringAttributedString に変更するだけで大幅に機能強化
      • 太字、斜体、下線、取り消し線、カスタムフォント、文字サイズ、前景色・背景色、カーニング、トラッキング、ベースラインオフセット、Genmoji をサポート
      • 加えて段落スタイリング(Line height / Text alignment / Writing direction)を新たにサポート
    • ダークモードと Dynamic Type に対応
    • システム UI による書式設定のトグル機能
  • AttributedString の基本
    • 文字列と属性の実行を含むシーケンス(属性実行)を格納
      • 属性実行の例:.largeTitle, .largtTitle + .orange
    • Swift の値型で UTF-8 エンコーディング使用
    • Equatable, Hashable, Codable, Sendable に準拠
    • カスタム属性、属性スコープの定義も可能

5:36 – Build custom controls

  • サンプルに追加機能の実装
    • 選択したテキストを材料リストに追加するボタンを作成
    • PreferenceKeyを使用してビュー階層での値の伝達
    • TextEdior$selection: AttributedTextSelection を介して選択内容を伝達
    • 単独の Range ではなく、RangeSet を使用した複数範囲の選択をサポート(双方向テキスト対応)
      • 複数言語 LtoR RtoL が混在し、跨いで選択した場合は複数の選択範囲として扱われるため
  • 双方向テキストの対応
    • 英語(左から右)とヘブライ語(右から左)のような混在テキスト
    • 視覚的な選択が複数の範囲に分割される場合に対応
    • RangeSet による不連続部分文字列のスライス機能
      • 例:.indices(where:\.isUppercase) により、すべての大文字を検出
      • text[uppercaseRanges].foreground = .blue で、大文字だけ青文字にすることが可能
  • カスタム属性の作成
    • IngredientAttribute によるテキスト範囲を材料としてマーク
    • AttributedString.Index はテキスト内のひとつの場所を表す
      • パフォーマンスのため、AttributedString ではコンテンツがツリー構造で保持、インデックスによりツリー内のパスが格納(16:25〜で図示)
      • このインデックスの動作により予想外のカーソル移動が発生してしまう
    • AttributedString indices の注意点:
      • 変更により全てのインデックスが無効化
      • 作成元の AttributedString でのみ使用可能
      • インデックスが無効になったことを検出した SwiftUI は、クラッシュ回避のためカーソルを末尾に移動する
    • AttributedString は テキストのUTF-8スカラ、UTF-16スカラへのビューが新たに得られるようになった。e.g. text.utf16[index]
    • テキスト変更時にインデックスと選択を更新
      • 範囲や範囲の配列を受け取る transform 関数による安全なインデックス更新 e.g. text.transform(updating: &cookingRange) { text in ... }
  • まとめ
    • for range in ranges のような Range のループはやめる
    • RangeSet を使った、ranges により一括でスタイル変更をする(スライス)
    • カーソル位置をテキスト変換と常に追従させるために、transform(updating:) を使用

22:02 – Define your text format

  • AttributedTextFormattingDefinition プロトコル
    • TextEditor が応答する AttributedStringKeys の定義
    • カスタムスコープでの属性制限(前景色、Genmoji、カスタム材料属性のみ許可)
    • .attributedTextFormattingDeinition(definition:) 修飾子でカスタム定義を TextEditor に渡す
    • AttributedStringKeys がスコープに含まれないものは、システム書式設定 UI に表示されない
  • AttributedTextValueConstraint プロトコル
    • AttributedTextValueConstriant.constrain(_:) で属性値の制約ロジックを実装
    • 例:材料属性がある場合は緑色、それ以外はデフォルト色
  • TextEditor による自動的な妥当性検証
    • ↑ の変更よりシステム書式設定 UI のカラーコントロールも無効になる
  • ペースト時もカスタム属性が維持される
  • カスタム属性の詳細制御
    • inheritedByAddedText:追加したテキストが属性継承をしなくなる
    • invalidationConditions:属性を削除する条件を定義(テキスト変更時など)
    • runBoundaries:属性の実行境界を制約(段落境界など)

34:08 – Next steps

  • サンプルプロジェクトの提供
    • SwiftUI の Transferable Wrapper によるドラッグ&ドロップや RTFD エクスポート
    • Swift Data を使用した AttributedString の永続化
  • AttributedString のオープンソース
    • Swift の Foundation プロジェクトの一部
    • GitHub での実装確認と貢献、Swift フォーラムでのコミュニティ交流
  • Genmoji サポート
    • 新しい TextEditor により Genmoji 入力サポートが簡単に追加可能
  • 開発のヒント
    • カスタム属性とフォーマット定義を組み合わせた高度なテキスト編集体験の構築