ハイライト
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 でついにリストがサポートされました。順序なし・順序付きリストに対応し、ローマ数字、英字、十進番号も含みます。システムはデバイス言語に基づいて番号形式を自動ローカライズします。
NSParagraphStyle の textLists プロパティでどの段落にリストスタイルを適用するか定義します。システムは改行で自動番号付けし、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 に準拠し、selectionRect と documentVisibleRect を実装し、スクロール開始/終了時に textInputClientWillStartScrollingOrZooming と willEndScrollingOrZooming を呼びます。
国際化の改善
動的行高
多くの言語では 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 プロパティを実装
関連セッション
- What’s new in UIKit — UIKit 新機能の概要
- What’s new in AppKit — AppKit 新機能。macOS テキストカーソル改善を含む
- Unleash the UIKit trait system — Trait システムのアップグレード。テキストレンダリングに関連
コメント
GitHub Issues · utterances