axiom-textkit-ref について
TextKit 2 のアーキテクチャ、TextKit 1 からの移行、Writing Tools 連携、SwiftUI TextEditor を扱う Swift 開発者向けのリファレンススキルです。ビューポートレイアウト、選択範囲、カスタムレイアウトフラグメントも解説します。
主な機能
- 01TextKit 2 の構造と主要型
- 02TextKit 1 からの移行ガイド
- 03ビューポートレイアウトとテキスト範囲
- 04Writing Tools との連携
- 05カスタムレイアウトと描画
活用シーン
- 01テキストビューを TextKit 2 に移行
- 02表示領域内のテキストレイアウトを管理
- 03Writing Tools の編集機能を統合
- 04独自のテキスト背景や添付要素を作成
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ charleswiltgen/ axiom --skill axiom-textkit-refインストール後、Claude Code で/axiom-textkit-refを入力して読み込まれたか確認します。
詳細
TextKit 2 リファレンス
TextKit 2 のアーキテクチャ、TextKit 1 からの移行、Writing Tools との統合、iOS 26 までの AttributedString を使った SwiftUI TextEditor を網羅した完全なリファレンスです。
アーキテクチャ
TextKit 2 は MVC パターンを採用し、正確性、安全性、パフォーマンスに最適化された新しいクラスを使用します。
モデル層
NSTextContentManager(抽象クラス)
- バッキングストアから NSTextElement オブジェクトを生成します
- ドキュメント内の要素範囲を追跡します
- デフォルトの実装:NSTextContentStorage
NSTextContentStorage
- NSTextStorage をバッキングストアとして使用します
- コンテンツを NSTextParagraph 要素に自動的に分割します
- テキストが変更されると、更新された要素を生成します
NSTextElement(抽象クラス)
- コンテンツの一部(段落、添付ファイル、カスタム型)を表します
- 不変の値セマンティクスを持ちます
- 作成後にプロパティを変更することはできません
- デフォルトの実装:NSTextParagraph
NSTextParagraph
- 1 つの段落を表します
- ドキュメント内の範囲を含みます
コントローラー層
NSTextLayoutManager
- TextKit 1 の NSLayoutManager を置き換えます
- グリフ API はありません(グリフを完全に抽象化します)
- 要素を受け取り、コンテナ内にレイアウトして、レイアウトフラグメントを生成します
- 常に非連続レイアウトを使用します
NSTextLayoutFragment
- 1 つ以上の要素に対する不変のレイアウト情報です
- 主なプロパティ:
textLineFragments— NSTextLineFragment の配列layoutFragmentFrame— コンテナ内のレイアウト境界renderingSurfaceBounds— 実際の描画境界(フレームを超える場合があります)
NSTextLineFragment
- 1 行のテキストに関する測定情報です
- 行数のカウントや幾何学的な問い合わせに使用します
ビュー層
NSTextViewportLayoutController
- ビューポートレイアウトの信頼できる情報源です
- 表示部分のみのレイアウトを調整します
- デリゲートメソッドを呼び出します:
willLayout、configureRenderingSurface、didLayout
NSTextContainer
- レイアウト先の幾何学的な情報を提供します
- 除外パス(非矩形レイアウト)を定義できます
オブジェクトベースの範囲
NSTextLocation(プロトコル)
- テキスト内の単一の位置を表します
- 整数インデックスを置き換えます
- 構造化ドキュメント(ネストされた要素を含む DOM など)をサポートします
NSTextRange
- 開始位置と終了位置(終了位置は含みません)
- ネストされた構造を表現できます
- 非線形ドキュメントでは NSRange と互換性がありません
NSTextSelection
- 次の情報を含みます:粒度、親和性、場合によっては分離した範囲
- 読み取り専用のプロパティ
- 不変の値セマンティクスを持ちます
NSTextSelectionNavigation
- 選択範囲に対する操作を実行します
- 新しい NSTextSelection インスタンスを返します
- 双方向テキストを正しく処理します
主な設計原則
1. 正確性 — グリフ API なし
WWDC 2021 より:
「TextKit 2 はグリフ処理を抽象化し、国際的なテキストに一貫した使用体験を提供します。」
グリフがない理由
問題: カンナダ語やアラビア語などの文字体系では:
- 1 つのグリフが複数の文字を表す場合があります(合字)
- 1 つの文字が複数のグリフに分かれる場合があります
- 字形形成の過程でグリフの順序が変わります
- 文字とグリフの正確な対応付けはできません
例(カンナダ語の単語「October」):
- 4 番目の文字が 2 つのグリフに分かれます
- 合字の適用前にグリフの順序が変わります
- 3 番目のグリフが結合形になり、別のグリフの下に移動します
解決策: グリフインデックスの代わりに、NSTextLocation、NSTextRange、NSTextSelection を使用します。
2. 安全性 — 値セマンティクス
不変オブジェクト:
- NSTextElement
- NSTextLayoutFragment
- NSTextLineFragment
- NSTextSelection
メリット:
- 意図しない共有がありません
- 変更による副作用がありません
- 状態を把握しやすくなります
パターン: レイアウトや選択範囲を変更するには、目的の変更を反映した新しいインスタンスを作成します。
3. パフォーマンス — ビューポートレイアウト
常に非連続レイアウト: TextKit 2 は、表示されているコンテンツとオーバースクロール領域のみをレイアウトします。
TextKit 1:
- 非連続レイアウトは任意(ブール値のプロパティ)
- レイアウト状態を確認できない
- レイアウトする部分を制御できない
TextKit 2:
- 常に非連続レイアウト
- ビューポートが表示領域を定義
- ビューポートの一貫したレイアウト情報
- ビューポートのレイアウト更新に関する通知
ビューポートのデリゲートメソッド:
textViewportLayoutControllerWillLayout(_:)— レイアウト前の準備textViewportLayoutController(_:configureRenderingSurfaceFor:)— フラグメントごとの処理textViewportLayoutControllerDidLayout(_:)— レイアウト後のクリーンアップ
TextKit 1 からの移行
主なパラダイムシフト
| TextKit 1 | TextKit 2 |
|---|---|
| グリフ | 要素 |
| NSRange | NSTextLocation/NSTextRange |
| NSLayoutManager | NSTextLayoutManager |
| グリフ API | グリフ API なし |
| 非連続レイアウトは任意 | 常に非連続レイアウト |
| NSTextStorage に直接アクセス | NSTextContentManager 経由 |
API 名の見分け方
WWDC 2022 より:
- 名前に
.offsetが含まれる → TextKit 1 - 名前に
.locationが含まれる → TextKit 2
NSRange ↔ NSTextRange の変換
NSRange → NSTextRange:
// UITextView/NSTextView
let nsRange = NSRange(location: 0, length: 10)
// Via content manager
let startLocation = textContentManager.location(
textContentManager.documentRange.location,
offsetBy: nsRange.location
)!
let endLocation = textContentManager.location(
startLocation,
offsetBy: nsRange.length
)!
let textRange = NSTextRange(location: startLocation, end: endLocation)
NSTextRange → NSRange:
let startOffset = textContentManager.offset(
from: textContentManager.documentRange.location,
to: textRange.location
)
let length = textContentManager.offset(
from: textRange.location,
to: textRange.endLocation
)
let nsRange = NSRange(location: startOffset, length: length)
グリフ API の置き換え
直接対応するグリフ API はありません。 より高レベルの構造を使用する必要があります。
例(TextKit 1 — 行数のカウント):
// TextKit 1 - iterate glyphs
var lineCount = 0
let glyphRange = layoutManager.glyphRange(for: textContainer)
for glyphIndex in glyphRange.location..<NSMaxRange(glyphRange) {
let lineRect = layoutManager.lineFragmentRect(
forGlyphAt: glyphIndex,
effectiveRange: nil
)
// Count unique rects...
}
置き換え(TextKit 2 — フラグメントの列挙):
// TextKit 2 - enumerate layout fragments
var lineCount = 0
textLayoutManager.enumerateTextLayoutFragments(
from: textLayoutManager.documentRange.location,
options: [.ensuresLayout]
) { fragment in
lineCount += fragment.textLineFragments.count
return true
}
互換モード(UITextView/NSTextView)
TextKit 1 への自動フォールバック:
.layoutManager プロパティにアクセスすると発生します。
警告(WWDC 2022):
「textView.layoutManager にアクセスすると、TK1 にフォールバックします」
フォールバックが発生すると:
- TextKit 2 に自動で戻る方法はありません
- 切り替えにはコストがかかります
- UI 状態(選択範囲、スクロール位置)が失われます
- 一方向の操作です
フォールバックを防ぐ:
- まず
.textLayoutManager(TextKit 2)を確認します else節でのみ.layoutManagerにアクセスします- TK1 が必要な場合は初期化時にオプトアウトします
// Check TextKit 2 first
if let textLayoutManager = textView.textLayoutManager {
// TextKit 2 code
} else if let layoutManager = textView.layoutManager {
// TextKit 1 fallback (old OS versions)
}
フォールバックのデバッグ:
- UIKit:
_UITextViewEnablingCompatibilityModeにブレークポイントを設定します - AppKit:
willSwitchToNSLayoutManagerNotificationを監視します
NSTextView のオプトイン(macOS)
TextKit 2 の NSTextView を作成する:
let textLayoutManager = NSTextLayoutManager()
let textContainer = NSTextContainer()
textLayoutManager.textContainer = textContainer
let textView = NSTextView(frame: .zero, textContainer: textContainer)
// textView.textLayoutManager now available
新しい便利なイニシャライザー:
// iOS 16+ / macOS 13+
let textView = UITextView(usingTextLayoutManager: true)
let nsTextView = NSTextView(usingTextLayoutManager: true)
デリゲートフック
NSTextContentStorageDelegate
ストレージを変更せずに属性をカスタマイズする:
func textContentStorage(
_ textContentStorage: NSTextContentStorage,
textParagraphWith range: NSRange
) -> NSTextParagraph? {
// Modify attributes for display
var attributedString = textContentStorage.attributedString!
.attributedSubstring(from: range)
// Add custom attributes
if isComment(range) {
attributedString.addAttribute(
.foregroundColor,
value: UIColor.systemIndigo,
range: NSRange(location: 0, length: attributedString.length)
)
}
return NSTextParagraph(attributedString: attributedString)
}
要素をフィルタリングする(コンテンツの表示/非表示):
func textContentManager(
_ textContentManager: NSTextContentManager,
shouldEnumerate textElement: NSTextElement,
options: NSTextContentManager.EnumerationOptions
) -> Bool {
// Return false to hide element
if hideComments && isComment(textElement) {
return false
}
return true
}
NSTextLayoutManagerDelegate
カスタムレイアウトフラグメントを提供する:
func textLayoutManager(
_ textLayoutManager: NSTextLayoutManager,
textLayoutFragmentFor location: NSTextLocation,
in textElement: NSTextElement
) -> NSTextLayoutFragment {
// Return custom fragment for special styling
if isComment(textElement) {
return BubbleLayoutFragment(
textElement: textElement,
range: textElement.elementRange
)
}
return NSTextLayoutFragment(
textElement: textElement,
range: textElement.elementRange
)
}
NSTextViewportLayoutController.Delegate
ビューポートレイアウトのライフサイクル:
func textViewportLayoutControllerWillLayout(_ controller: NSTextViewportLayoutController) {
// Prepare for layout: clear sublayers, begin animation
}
func textViewportLayoutController(
_ controller: NSTextViewportLayoutController,
configureRenderingSurfaceFor textLayoutFragment: NSTextLayoutFragment
) {
// Update geometry for each visible fragment
let layer = getOrCreateLayer(for: textLayoutFragment)
layer.frame = textLayoutFragment.layoutFragmentFrame
// Animate to new position if needed
}
func textViewportLayoutControllerDidLayout(_ controller: NSTextViewportLayoutController) {
// Finish: commit animations, update scroll indicators
}
実践的なパターン
カスタムレイアウトフラグメント(吹き出しの背景)
class BubbleLayoutFragment: NSTextLayoutFragment {
override func draw(at point: CGPoint, in context: CGContext) {
// Draw custom background
context.setFillColor(UIColor.systemIndigo.cgColor)
let bubblePath = UIBezierPath(
roundedRect: layoutFragmentFrame,
cornerRadius: 8
)
context.addPath(bubblePath.cgPath)
context.fillPath()
// Draw text on top
super.draw(at: point, in: context)
}
}
レンダリング属性(一時的なスタイル設定)
テキストストレージを変更せずに属性を追加する:
textLayoutManager.addRenderingAttribute(
.foregroundColor,
value: UIColor.green,
for: ingredientRange
)
// Remove when no longer needed
textLayoutManager.removeRenderingAttribute(
.foregroundColor,
for: ingredientRange
)
UIView を使ったテキスト添付ファイル
// iOS 15+
let attachment = NSTextAttachment()
attachment.image = UIImage(systemName: "star.fill")
// Provide view for interaction
class AttachmentViewProvider: NSTextAttachmentViewProvider {
override func loadView() {
super.loadView()
let button = UIButton(type: .system)
button.setTitle("Tap me", for: .normal)
button.addTarget(self, action: #selector(didTap), for: .touchUpInside)
view = button
}
@objc func didTap() {
// Handle tap
}
}
リストとテーブル
// Create list
let listItem = NSTextList(markerFormat: .disc, options: 0)
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.textLists = [listItem]
attributedString.addAttribute(
.paragraphStyle,
value: paragraphStyle,
range: range
)
NSTextList は UIKit で利用できます(iOS 16 以降)。以前は AppKit のみでした。
ヒットテストと選択範囲のジオメトリ
// Get text range at point
let location = textLayoutManager.location(
interactingAt: point,
inContainerAt: textContainer.location
)
// Get bounding rect for range
var boundingRect = CGRect.zero
textLayoutManager.enumerateTextSegments(
in: textRange,
type: .standard,
options: []
) { segmentRange, segmentRect, baselinePosition, textContainer in
boundingRect = boundingRect.union(segmentRect)
return true
}
Writing Tools(iOS 18 以降)
基本的な統合(TextKit 2 が必要)
WWDC 2024 より:
「Writing Tools のすべての機能をサポートするには、UITextView または NSTextView で TextKit 2 を使用する必要があります。TextKit 1 を使用している場合は、書き換え結果をパネルに表示するだけの限定的な機能になります。」
ネイティブのテキストビューでは無料で利用可能:
// UITextView, NSTextView, WKWebView
// Writing Tools appears automatically
ライフサイクルのデリゲートメソッド
func textViewWritingToolsWillBegin(_ textView: UITextView) {
// Pause syncing, prevent edits
isSyncing = false
}
func textViewWritingToolsDidEnd(_ textView: UITextView) {
// Resume syncing
isSyncing = true
}
// Check if active
if textView.isWritingToolsActive {
// Don't persist text storage
}
動作の制御
// Opt out completely
textView.writingToolsBehavior = .none
// Panel-only experience (no in-line edits)
textView.writingToolsBehavior = .limited
// Full experience (default)
textView.writingToolsBehavior = .default
結果のオプション
// Plain text only
textView.writingToolsResultOptions = [.plainText]
// Rich text
textView.writingToolsResultOptions = [.richText]
// Rich text + tables
textView.writingToolsResultOptions = [.richText, .table]
// Rich text + lists
textView.writingToolsResultOptions = [.richText, .list]
保護された範囲
// UITextViewDelegate / NSTextViewDelegate
func textView(
_ textView: UITextView,
writingToolsIgnoredRangesIn enclosingRange: NSRange
) -> [NSRange] {
// Return ranges that Writing Tools should not modify
return codeBlockRanges + quoteRanges
}
WKWebView: <blockquote> タグと <pre> タグは自動的に無視されます。
Writing Tools Coordinator(iOS 26 以降)
カスタムテキストエンジン向けの高度な統合です。
セットアップ
// UIKit
let coordinator = UIWritingToolsCoordinator()
coordinator.delegate = self
textView.addInteraction(coordinator)
coordinator.writingToolsBehavior = .default
coordinator.writingToolsResultOptions = [.richText]
// AppKit
let coordinator = NSWritingToolsCoordinator()
coordinator.delegate = self
customView.writingToolsCoordinator = coordinator
Coordinator のデリゲート
コンテキストを提供する:
func writingToolsCoordinator(
_ coordinator: NSWritingToolsCoordinator,
requestContexts scope: NSWritingToolsCoordinator.ContextScope
) async -> [NSWritingToolsCoordinator.Context] {
// Return attributed string + selection range
let context = NSWritingToolsCoordinator.Context(
attributedString: currentText,
range: currentSelection
)
return [context]
}
変更を適用する:
func writingToolsCoordinator(
_ coordinator: NSWritingToolsCoordinator,
replace context: NSWritingToolsCoordinator.Context,
range: NSRange,
with attributedString: NSAttributedString
) async {
// Update text storage
textStorage.replaceCharacters(in: range, with: attributedString)
}
選択範囲を更新する:
func writingToolsCoordinator(
_ coordinator: NSWritingToolsCoordinator,
updateSelectedRange selectedRange: NSRange,
in context: NSWritingToolsCoordinator.Context
) async {
// Update selection
self.selectedRange = selectedRange
}
アニメーション用のプレビューを提供する:
// macOS
func writingToolsCoordinator(
_ coordinator: NSWritingToolsCoordinator,
previewsFor context: NSWritingToolsCoordinator.Context,
range: NSRange
) async -> [NSTextPreview] {
// Return one preview per line for smooth animation
return textLines.map { line in
NSTextPreview(
image: renderImage(for: line),
frame: line.frame
)
}
}
// iOS
func writingToolsCoordinator(
_ coordinator: UIWritingToolsCoordinator,
previewFor context: UIWritingToolsCoordinator.Context,
range: NSRange
) async -> UITargetedPreview {
// Return single preview
return UITargetedPreview(
view: previewView,
parameters: parameters
)
}
校正マーク:
func writingToolsCoordinator(
_ coordinator: NSWritingToolsCoordinator,
underlinesFor context: NSWritingToolsCoordinator.Context,
range: NSRange
) async -> [NSValue] {
// Return bezier paths for underlines
return ranges.map { range in
let path = bezierPath(for: range)
return NSValue(bytes: &path, objCType: "CGPath")
}
}
PresentationIntent(iOS 26以降)
セマンティックなリッチテキストの結果オプション:
coordinator.writingToolsResultOptions = [.richText, .presentationIntent]
表示属性との違い:
表示属性(太字、イタリック):
- 具体的なフォント情報(ポイントサイズ、フォント名)
- セマンティックな意味はありません
PresentationIntent(ヘッダー、コードブロック、強調):
- セマンティックなスタイル情報
- アプリが内部スタイルに変換します
- リスト、テーブル、コードブロックではPresentationIntentを使用します
- 下線、下付き文字、上付き文字では引き続き表示属性を使用します
例:
// Check for presentation intent
if attributedString.runs[\.presentationIntent].contains(where: { $0?.components.contains(.header(level: 1)) == true }) {
// This is a heading
}
SwiftUI TextEditor + AttributedString(iOS 26以降)
基本的な使い方
struct RecipeEditor: View {
@State private var text: AttributedString = "Recipe text"
var body: some View {
TextEditor(text: $text)
}
}
サポートされている属性:
- 太字、イタリック、下線、取り消し線
- カスタムフォント、ポイントサイズ
- 前景色と背景色
- カーニング、トラッキング、ベースラインオフセット
- Genmoji
- 行の高さ、テキスト配置、基本の書字方向
TextAlignmentとWritingDirection
// Text alignment (AttributedString.TextAlignment)
var text = AttributedString("Centered paragraph")
text.alignment = .center // .left, .right, .center
// Writing direction for bidirectional text
var bidiText = AttributedString("Hello عربي")
bidiText.writingDirection = .rightToLeft // .leftToRight, .rightToLeft
行の高さの制御
var multiline = AttributedString("Paragraph\nwith multiple\nlines.")
multiline.lineHeight = .exact(points: 32) // Fixed height
multiline.lineHeight = .multiple(factor: 2.5) // Multiplier
multiline.lineHeight = .loose // System loose spacing
選択範囲のバインディング
@State private var selection: AttributedTextSelection?
TextEditor(text: $text, selection: $selection)
AttributedTextSelection:
enum AttributedTextSelection {
case none
case single(NSRange)
case multiple(Set<NSRange>) // For bidirectional text
}
選択したテキストの取得:
if let selection {
let selectedText: AttributedSubstring
switch selection.indices {
case .none:
selectedText = text[...]
case .single(let range):
selectedText = text[range]
case .multiple(let ranges):
// Discontiguous substring from RangeSet
selectedText = text[selection]
}
}
選択範囲のプログラムによる置換
var text = AttributedString("Here is my dog")
var selection = AttributedTextSelection(range: text.range(of: "dog")!)
// Replace with plain text
text.replaceSelection(&selection, withCharacters: "cat")
// Replace with attributed content
let replacement = AttributedString("horse")
text.replaceSelection(&selection, with: replacement)
DiscontiguousAttributedSubstring
RangeSetを使って、連続していない選択範囲を扱います:
let text = AttributedString("Select multiple parts of this text")
let range1 = text.range(of: "Select")!
let range2 = text.range(of: "text")!
let rangeSet = RangeSet([range1, range2])
var substring = text[rangeSet] // DiscontiguousAttributedSubstring
substring.backgroundColor = .yellow
// Convert back to AttributedString
let combined = AttributedString(substring)
テキスト選択のアフィニティ
ビュー階層の選択アフィニティを制御します:
TextEditor(text: $text, selection: $selection)
.textSelectionAffinity(.upstream) // .upstream or .downstream
行境界で選択範囲をテキストの先頭側に解決する場合は、.upstreamを使用します。
カスタム書式設定の定義
編集可能な属性を制限します:
struct RecipeFormattingDefinition: AttributedTextFormattingDefinition {
typealias FormatScope = RecipeAttributeScope
static let constraints: [any AttributedTextValueConstraint<RecipeFormattingDefinition>] = [
IngredientsAreGreen()
]
}
struct RecipeAttributeScope: AttributedScope {
var ingredient: IngredientAttribute
var foregroundColor: ForegroundColorAttribute
var genmoji: GenmojiAttribute
}
TextEditorに適用します:
TextEditor(text: $text)
.attributedTextFormattingDefinition(RecipeFormattingDefinition.self)
値の制約
カスタムロジックに基づいて属性値を制御します:
struct IngredientsAreGreen: AttributedTextValueConstraint {
typealias Definition = RecipeFormattingDefinition
typealias AttributeKey = ForegroundColorAttribute
func constrain(
_ value: inout Color?,
in scope: RecipeFormattingDefinition.FormatScope
) {
if scope.ingredient != nil {
value = .green // Ingredients are always green
} else {
value = nil // Others use default
}
}
}
システムの動作:
- TextEditorは制約を調べ、変更が有効かどうかを判断します
- 制約によって変更が元に戻される場合、コントロールは無効になります
- 制約はペーストしたコンテンツにも適用されます
カスタム属性
属性を定義します:
struct IngredientAttribute: CodableAttributedStringKey {
typealias Value = UUID // Ingredient ID
static let name = "ingredient"
}
extension AttributeScopes.RecipeAttributeScope {
var ingredient: IngredientAttribute.Type { IngredientAttribute.self }
}
属性の動作:
extension IngredientAttribute {
// Don't expand when typing after ingredient
static let inheritedByAddedText = false
// Remove if text in run changes
static let invalidationConditions: [AttributedString.InvalidationCondition] = [
.textChanged
]
// Optional: constrain to paragraph boundaries
static let runBoundaries: AttributedString.RunBoundaries = .paragraph
}
AttributedStringの変更
安全なインデックス更新:
// Transform updates indices/selection during mutation
text.transform(updating: &selection) { mutableText in
// Find ranges
let ranges = mutableText.characters.ranges(of: "butter")
// Set attribute for all ranges at once
for range in ranges {
mutableText[range].ingredient = ingredientID
}
}
// selection is now updated to match transformed text
古いインデックスは使用しないでください:
// BAD - indices invalidated by mutation
let range = text.characters.range(of: "butter")!
text[range].foregroundColor = .green
text.append(" (unsalted)") // range is now invalid!
AttributedStringのビュー
同じコンテンツへの複数のビュー:
characters— 書記素クラスタunicodeScalars— Unicodeスカラーutf8— UTF-8コード単位utf16— UTF-16コード単位
すべてのビューで同じインデックスを共有します。
既知の制限事項と注意点
ビューポートのスクロールに関する問題
専門家の記事より:
- ビューポートによってスクロール位置が不安定になることがあります
- スクロール中に
usageBoundsForTextContainerが変化します - AppleのTextEditでも同じ問題が発生します
- パフォーマンス向上とのトレードオフです
TextKit 1との互換性
.layoutManagerにアクセスするとフォールバックが発生します- 一方向の操作で、自動的に戻ることはありません
- 切り替え時にUIの状態が失われます
- レイアウトシステムの切り替えにはコストがかかります
AttributedStringのインデックス無効化
- 変更を行うと、すべてのインデックスが無効になります
- インデックスを有効なまま保つには
.transform(updating:)を使用する必要があります - インデックスは、元のAttributedStringでのみ機能します
TextKit 1のサポート範囲の制限
TextKit 2ではサポートされていないもの:
- NSTextTable(NSTextListまたはカスタムレイアウトを使用してください)
- 一部のレガシーなテキスト添付ファイル
- グリフの直接操作
リソース
WWDC:2021-10061, 2022-10090, 2023-10058, 2024-10168, 2025-265, 2025-280
ドキュメント:/uikit/nstextlayoutmanager, /appkit/textkit/using_textkit_2_to_interact_with_text, /uikit/display-text-with-a-custom-layout, /swiftui/building-rich-swiftui-text-experiences, /foundation/attributedstring, /foundation/attributedstring/textalignment, /foundation/attributedstring/lineheight, /foundation/discontiguousattributedsubstring, /uikit/writing-tools, /appkit/enhancing-your-custom-text-engine-with-writing-tools