WWDC Quick Look 💓 By SwiftGGTeam
What's new with text and text interactions

What's new with text and text interactions

元の動画を見る

ハイライト

iOS 17 と macOS Sonoma はテキスト選択 UI の全面刷新、UITextSelectionDisplayInteraction による軽量な選択ソリューション、カスタマイズ可能なテキスト項目インタラクション、TextKit 2 のリスト対応、多言語向けの動的行高と改行改善をもたらし、リッチテキスト体験と国際化サポートを強化します。

主要内容

選択 UI の全面再設計

すべてのプラットフォームで新しいテキストカーソルが導入されました。入力言語を切り替えると、カーソル位置にインラインのインタラクティブな切り替え UI が表示されます。範囲選択時のハンドルはより人間工学に配慮した形状になり、長文でカーソルを正確に置くための新しいルーペ(loupe)も追加されました。

UITextView と UITextField を使うアプリはこれらの更新を自動的に取得します。UITextInteraction を使うカスタムテキストビューも新しい選択 UI の恩恵を受けます。

UITextSelectionDisplayInteraction:ジェスチャーを引き継がない選択 UI

高度にカスタマイズされたテキスト UI を持ち、UITextInteraction(独自のジェスチャー処理を含む)を採用できないアプリ向けに、iOS 17 では UITextSelectionDisplayInteraction が提供されます。カーソル、カーソル添付、選択ハイライト、選択ハンドルなど選択 UI のみを提供し、ジェスチャーは含みません。

任意の UIView にインストールできる新しい UIInteraction サブクラスです。選択状態を提供する UITextInput 準拠オブジェクトが必要です。選択状態が変わったら setNeedsSelectionUpdate() を呼び、インタラクションがすべてのビューを自動更新します。

必要に応じてすべての選択ビューを置き換えられます。

UITextLoupeSession:独立したルーペ

UITextSelectionDisplayInteraction に加え、iOS 17 では独立したルーペ API が追加されました。任意のビューで使え、UITextSelectionDisplayInteraction や UITextInput は不要です。

UIPanGestureRecognizer での駆動が推奨されます。ジェスチャー開始時に UITextLoupeSession.begin(at:)、移動中に move(to:)、終了時に invalidate() を呼びます。

テキスト項目インタラクションの強化

UITextView 内のリンク、添付、カスタムテキスト範囲のインタラクションを完全にカスタマイズできます。UITextViewDelegate に primary action のカスタマイズやメニュー提供のメソッドが追加されました。

テキスト項目には以下が含まれます:

  • NSTextAttachment(添付)
  • NSLinkAttributeName(リンク)
  • UITextItemTagAttributeName(カスタム範囲)

UITextItemTagAttributeName で任意のテキスト範囲をインタラクティブ項目としてマークできます。menuConfigurationFor:defaultMenu: でカスタムメニュー、primaryAction でデフォルトのタップ動作を変更します。nil を返すとデフォルト動作を無効化します。

TextKit 2 のリスト対応

TextKit 2 でついにリストがサポートされました。順序なし・順序付きリストに対応し、ローマ数字、英字、十進番号も含みます。システムはデバイス言語に基づいて番号形式を自動ローカライズします。

NSParagraphStyletextLists プロパティでどの段落にリストスタイルを適用するか定義します。システムは改行で自動番号付けし、UITextView は typing attributes で段落スタイルを自動伝播します。

macOS のディクテーション UI

macOS Sonoma では新しいディクテーションインジケーターが導入されました。話している間は末尾にグロー効果、静止時はマイクアイコンが表示されます。スクロール中はインジケーターがスクロールビューの端に吸着し、現在の入力位置に戻るボタンを提供します。

標準の NSTextView は自動的にこれらの動作を取得します。カスタムテキストビューは自前の挿入点描画を NSTextInsertionIndicator に置き換えるべきです。外観(色、サイズ)を調整できる NSView サブクラスで、デフォルトではアクセントカラーに追従します。

effectsViewInserter ブロックを設定し、システムが効果ビューをドキュメントビュー階層に挿入できるようにします。システムは効果ビューを配置し、カーソル移動に合わせて更新します。テキストビューが first responder を resign したら displayMode.hidden にしてカーソルを非表示にします。

ディクテーション中のテキスト挿入時、フレーム更新でグローが自動アニメーションします。グローが不要なら automaticModeOptions から .showEffectsView を削除します。

入力モード切り替え時、カーソル下に言語選択 UI が表示されます。位置を上書きする場合は NSTextInputClient 実装で preferredTextAccessoryPlacement を指定します。

カーソルが画面外にスクロールすると、scroll-away indicator が相対位置を示し、ディクテーション位置に戻る入口を提供します。カスタムビューは NSTextInputClient に準拠し、selectionRectdocumentVisibleRect を実装し、スクロール開始/終了時に textInputClientWillStartScrollingOrZoomingwillEndScrollingOrZooming を呼びます。

国際化の改善

動的行高

多くの言語では ascender と descender がラテン文字の行高を超え、文字が切り取られることがあります。iOS 17 は UILabel と UITextField の行高を自動調整してこれらの言語に対応します。

この動作は特定条件でのみ発動します:

  • デバイスが追加行高を必要とする言語で構成されている
  • 英語など固定行高の言語も含め UI 全体に影響し、視覚的一貫性を保つ
  • text style を使うテキスト要素のみ対象。カスタムフォントは固定行高のまま

この動作を得るには:

  • preferredFont(forTextStyle:) でフォントを作成
  • テキスト要素に clipsToBounds を設定しない
  • UI が高さ変化に応答し、他のコントロールが整列したままであることを確認

改行の改善

中国語、ドイツ語、日本語、韓国語の改行ルールが大幅に改善され、text style ごとに異なるルールが適用されます。

韓国語の例:以前はタイトル内の単語が行をまたいで分割されることがありましたが、title text style では単語内改行を防ぎ、単語境界でのみ改行します。body text style では引き続き単語内改行を許可します。

ドイツ語では、狭いレイアウトで長い単語が文字単位で改行されていましたが、形態素境界で自動断字され、よりバランスの取れたレイアウトになります。

これらの改善を得るには text style を使用します。

詳細

選択表示インタラクションの追加

01:46

// 選択表示インタラクションを作成し、UITextInput 実装を提供
let selectionInteraction = UITextSelectionDisplayInteraction(textInput: document)

// 選択 UI を表示するビューに追加
textContainerView.addInteraction(selectionInteraction)

// 選択状態が変わったときに更新
document.selectionChanged = {
    selectionInteraction.setNeedsSelectionUpdate()
}

キーポイント:

  • UITextSelectionDisplayInteraction は選択 UI のみ提供し、ジェスチャーは含まない
  • 選択状態を提供する UITextInput オブジェクトが必要
  • 選択状態変化時に setNeedsSelectionUpdate() を呼ぶ
  • すべての選択ビューを置き換え可能

ルーペセッションの管理

03:16

var loupeSession: UITextLoupeSession?

@objc func handlePanGesture(_ gesture: UIPanGestureRecognizer) {
    let location = gesture.location(in: textView)
    let selectionWidget = cursorView
    
    switch gesture.state {
    case .began:
        loupeSession = UITextLoupeSession.begin(
            at: location,
            in: selectionWidget,
            from: textView
        )
    case .changed:
        loupeSession?.move(to: location)
    case .ended, .cancelled:
        loupeSession?.invalidate()
        loupeSession = nil
    default:
        break
    }
}

キーポイント:

  • UITextLoupeSession.begin(at:in:from:) でルーペセッションを作成
  • move(to:) でジェスチャーに合わせて位置を更新
  • invalidate() で終了時にセッションを破棄
  • テキストビューに限定されず任意のビューで使用可能

テキスト項目のメニュー設定

05:56

func textView(
    _ textView: UITextView,
    menuConfigurationFor textItem: UITextItem,
    defaultMenu: UIMenu
) -> UITextItem.MenuConfiguration? {
// nil を返してデフォルトメニューを無効化
// カスタムメニュー設定を返す
    return .init(menu: UIMenu(children: [
        UIAction(title: "Open in App") { _ in
            self.handleLink(textItem.link)
        }
    ]))
}

キーポイント:

  • menuConfigurationFor:defaultMenu: でテキスト項目のメニューをカスタマイズ
  • nil を返すとデフォルト動作を無効化
  • インラインプレビューを含められる

カスタムテキスト範囲をインタラクティブにマーク

05:32

let attributedString = NSMutableAttributedString(string: "Tap here for more info")
attributedString.addAttribute(
    UITextItemTagAttributeName,
    value: "info-tag",
    range: NSRange(location: 0, length: 8)
)

キーポイント:

  • UITextItemTagAttributeName で任意のテキスト範囲をインタラクティブにマーク
  • マークした範囲は text item の delegate コールバックをトリガー
  • リンク以外のテキストにインタラクションを追加する用途

TextKit 2 リスト

06:46

let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.textLists = [NSTextList(markerFormat: .decimal, options: 0)]

let attributedString = NSMutableAttributedString(string: "First item\nSecond item\nThird item")
attributedString.addAttribute(
    .paragraphStyle,
    value: paragraphStyle,
    range: NSRange(location: 0, length: attributedString.length)
)

キーポイント:

  • NSParagraphStyle.textLists でリストスタイルを定義
  • NSTextList(markerFormat:options:) でリスト形式を作成
  • .decimal.lowercaseLetter.uppercaseLetter.lowercaseRoman.uppercaseRoman.bullet をサポート
  • システムが locale に基づいてローカライズ
  • UITextView が typing attributes を自動伝播

macOS テキスト挿入インジケーター

08:28

let insertionIndicator = NSTextInsertionIndicator()
documentView.addSubview(insertionIndicator)

insertionIndicator.effectsViewInserter = { view, placement in
    documentView.addSubview(view)
}

// カーソルを非表示
textViewDidResignFirstResponder {
    insertionIndicator.displayMode = .hidden
}

キーポイント:

  • NSTextInsertionIndicator がカスタムカーソル描画を置き換え
  • effectsViewInserter でシステムがディクテーション効果ビューを挿入
  • displayMode で表示/非表示を制御
  • デフォルトでアクセントカラーに追従

重要ポイント

1. カスタム選択 UI をシステムコンポーネントに置き換える

  • やること: カスタムテキストビュー内のカーソル、選択ハンドル、ルーペをシステム提供コンポーネントに交換
  • 価値: システムスタイル更新に自動追従、保守コスト削減、ディクテーションのグロー効果など新機能を取得
  • 始め方: iOS では UITextSelectionDisplayInteraction、macOS では NSTextInsertionIndicator

2. テキスト内のキーワードにインタラクションメニューを追加

  • やること: UITextView 内の特定キーワード(人名、場所、製品名)にカスタムメニューを追加
  • 価値: 長押しで関連操作を実行でき、現在の画面を離れない
  • 始め方: UITextItemTagAttributeName で範囲をマークし、menuConfigurationFor:defaultMenu: を実装

3. リッチテキストエディターにリスト対応を追加

  • やること: テキストエディターに順序付き/順序なしリスト機能を追加
  • 価値: TextKit 2 ネイティブ対応で自動番号付けとローカライズ。リスト状態を手動管理不要
  • 始め方: NSParagraphStyle.textLists を設定し、NSTextList で形式を定義

4. 多言語レイアウトでテキストが切り取られないようにする

  • やること: UILabel と UITextField が text style を使い、clipsToBounds を設定していないか確認
  • 価値: タイ語、ヒンディー語などは標準行高を超えるため、切り取りは可読性を損なう
  • 始め方: カスタムフォントを preferredFont(forTextStyle:) に置き換え、clipsToBounds = true を削除

5. macOS テキストエディターにディクテーション対応を追加

  • やること: カスタム macOS テキストビューでシステムディクテーションをサポート
  • 価値: 音声入力でグロー効果、端への吸着、scroll-away indicator などネイティブ体験
  • 始め方: カスタムカーソルを NSTextInsertionIndicator に置き換え、effectsViewInserter を設定し、NSTextInputClient プロパティを実装

関連セッション

コメント

GitHub Issues · utterances