-
AppKitアプリのモダナイズ
AppKitアプリを、macOSにおける最新のプラクティスに合わせてモダナイズしましょう。コントロールイベントやジェスチャリコグナイザによる入力の処理を、従来のトラッキングループより進化した方法で実行する方法を解説します。アプリのキーボードナビゲーションの強化、再起動後のグレースフルな状態復元の実装や、インターフェイスをmacOSのデザインとシームレスに調和させる、コーナー同心性に関する新しいAPIの活用方法も紹介します。
関連する章
- 0:00 - Introduction
- 1:06 - Modern input
- 1:27 - Modern event handling with gesture recognizers
- 2:25 - Selection, context menus, and drag and drop
- 3:52 - Text selection in custom views
- 4:26 - Control events and gesture recognizers
- 5:51 - Keyboard navigation and status items
- 8:57 - Continuity across launches
- 9:08 - Graceful app termination
- 9:55 - State restoration
- 14:09 - Design updates
- 14:24 - Liquid Glass updates in macOS 27
- 15:41 - Concentricity
- 16:59 - Next steps
リソース
- Use SwiftUI with AppKit
- Restoring your app’s state with AppKit
- Gestures
- TN3212: Adopting gesture recognizers for Sidecar touch support
- NSControl.Events
関連ビデオ
WWDC26
-
このビデオを検索
こんにちは Mac UIフレームワークエンジニアのUjjainiです。 「Modernize Your AppKit App」です。 モダンなアプリは AppKitとMacの連携を活かし その形式と機能がシステム全体と 調和するよう設計されています。 その調和は3つの面で現れます。 アプリの操作方法と システムによる管理方法 そして画面上の 見た目と使い心地です。 本日は3つのヒントをご紹介します。 まず、アプリが対応すべき モダンな高精度入力方法を説明します。 次に、起動間の継続性を 保つことの重要性を説明します。 つまり、システムの要求に応じて アプリが適切に終了し 終了前の状態に戻れるようにする方法です。 最後に、macOS 27における 見た目と使い心地のアップデートを紹介します。 その多くはアプリを リビルドしなくても確認できます。
Macの原点である 入力デバイスから始めましょう。 高精度入力デバイスは Macの誕生当初からその核心にあります。 初代Macには キーボードとマウスが付属していました。 時を経て、様々な入力種別を 処理するAPIは進化し はるかに使いやすくなりました。
mouseDownとトラッキングループは 定番のパターンでしたが AppKitアプリでインタラクティブな 動作を実装する際に使われてきました。 しかし、Mac上のフレームワークは AppKitだけではありません。 SwiftUI、Mac Catalyst経由のUIKit そしてAppKitが連携して Macの体験を作り出しています。 これらはGesture Recognizerを活用して 3つのフレームワーク全体で 共通のイベント処理言語を提供しています。 Gesture RecognizerはAppKitに 高度な動作を提供する力を与え 自前で実装する必要がありません。 イベントを扱うモダンな方法は Gesture Recognizerです。 Gesture Recognizerとの連携に 優れた3つの解決策を紹介します。 ビューベースのAPI、コントロールイベント そしてカスタムのGesture Recognizer自体です。 これらはトラッキングループや mouseDownオーバーライドと同等のカスタマイズ性を持ち クロスフレームワーク互換性もサポートします。 それぞれの使いどころをご説明します。
mouseDownの一般的なオーバーライドは 選択のトラッキング コンテキストメニューの表示 ドラッグ&ドロップ、テキスト選択に使われます。 現在mouseDownをオーバーライドしている場合 AppKitにはこれらの動作をより確実に 処理する専用APIがあります。 モダンなMacプラットフォームを 最大限に活用できます。
mouseDownは選択のトラッキングのために オーバーライドされることがよくあります。 代わりに NSCollectionViewItemやNSTableRowViewなどの selectedプロパティを監視してください。 または、選択変更時に通知される デリゲートコールバックを使用します。 NSTableViewDelegateや NSOutlineViewDelegateなどです。 ビューからコンテキストメニューを 表示する方法はいくつかあります。
NSViewのクラスプロパティ .defaultMenuを使用すると そのビューのすべてのインスタンスで 同じメニューが表示されます。 NSResponderのインスタンスプロパティ .menuを使用すると レスポンダーごとに異なるメニューを 提供できます。 またはNSViewのインスタンスメソッド .menuForEventを使用すると イベントに基づいて メニューを動的に作成できます。 アプリでコレクションコンテナビューを 使用している場合は tableView pasteboardWriterForRowのような モダンなドラッグデリゲートメソッドを使用します。 pasteboardItemを作成し データを設定して返します。 NSCollectionView、NSOutlineView、 NSBrowserにも同様のメソッドがあります。
NSTextView以外でテキスト選択の 動作が必要な場合は NSTextSelectionManagerを使用します。 macOS 27の新しいAPIで Gesture Recognizerを活用し クラシックなmacOSのテキスト選択動作を 任意のビューにもたらします。 ビューにアタッチして テキスト選択データソースを設定します。 これにより双方向選択のサポートが得られます。 テキストのドラッグ&ドロップ 切り替え、その他多くの機能も使えます。
次の解決策は少し見覚えがあるかもしれません。 UIKitのコントロールイベントを ご存知の方には。 コントロールイベントがAppKitに登場しました。 コントロールイベントは標準的な Macコントロールに追加できます。 ボタンやスライダーなどです。 ユーザー操作によるトラッキング状態の変化に コードで反応できるようになります。 複雑なmouseDownトラッキングロジックを 実装する必要はありません。 コントロールイベントが発生すると AppKitが登録済みのターゲットとアクションを呼び出します。 これらのコントロールイベントのほとんどは OS 10.11から利用可能です。
NSControlEventsの例を示します。 ボタンをインスタンス化し コントロールイベントにターゲットとアクションを登録します。 動作させるためにNSButtonを サブクラス化する必要もありません。 ビュー内のインタラクションを より細かく制御するには 標準のGesture Recognizerを追加します。 さらに柔軟性が必要な場合は カスタムGesture Recognizerサブクラスを作成します。 詳細は「Gestures」ドキュメントをご覧ください。 Gesture Recognizerはビューと そのサブビューに対して動作するため 重なり合う兄弟ビューが マウスイベントをサイレントにブロックする場合があります。 コントロールがクリックに 応答しないように見える場合は そのボタンと重なる兄弟ビューが ないか確認してください。 この問題に対処するには 重ならないようにビューのサイズを変更します。 オーバーレイのためにビューの サイズを変更できない場合は hitTestをオーバーライドしてnilを返し ヒットテストが下のコンテンツに フォールスルーできるようにします。
次はキーボードナビゲーションについてです。 アプリはキーボード入力に シームレスに応答する必要があります。 速度とアクセシビリティのためです。 コントロールのキーボードナビゲーションは システム設定でオンにできます。 オンにすると、TabキーまたはShift+Tabで コントロール間をフォーカス移動できます。 キービューループはTabキーを押したときに コントロールが順に巡回される順序です。 Tabキーが押されたときの順序です。 ループを自動的に再計算するには ビュー階層でビューが追加または 削除されるたびに ウインドウの.autorecalculatesKeyViewLoopを 有効にします。 この値を設定しない場合は キービューループの作成と維持を 自分で行う必要があります。
キーボードナビゲーションはアプリの ウインドウを超えて メニューバーとステータスアイテムにも及びます。 ステータスアイテム間のナビゲーションは メインメニューアイテムとは少し異なります。 クリックするとメニューを表示する ステータスアイテムは すでにメニューバーのメニューと同様に動作します。 ただし、ステータスアイテムはトリガにもなり アクションを実行したり 何らかの一時的なUIを表示したりできます。 アクションをトリガするには NSStatusItemのbuttonプロパティを変更して ターゲットとアクション、および オプションで画像を含めます。 これは通常のボタンと同様に動作し Returnキーを押すと自動的にアクションが実行されます。 キーボードナビゲーション中に Returnキーが押されたときです。 ステータスメニューアイテムに カスタムビューを使用するには ステータスアイテムのviewプロパティを使って ビューを設定します。 次にステータスアイテムに ターゲットとアクションを追加して そのアクションの実行を 有効にします。 ステータスアイテムはカスタムウインドウを 表示するトリガにもなります。 ステータスアイテムがウインドウを表示するとき AppKitはそのUIが アクティブな時期を知る必要があります。 キーボードフォーカスが 正しく動作するためです。 expanded interface session APIを使って カスタムUIのライフサイクルを追跡します。 まずデリゲートを設定します。 アイテムが作成されるときに beginとendの呼び出しを受け取るデリゲートです。 ウインドウを表示または 非表示にするためのものです。 デリゲートでstatusItem didBegin ExpandedInterfaceSessionを実装します。 そしてstatusItemDidEnd_ ExpandedInterfaceSessionも実装します。 これらのメソッドはAppKitが呼び出して expanded interface sessionの ライフサイクルを管理します。 didBeginの呼び出しでウインドウを表示します。 didEndの呼び出しでウインドウを非表示にします。
セッションを閉じる時が来たら 例えばアクションが選択されたときに .expandedInterfaceSession?の .cancelを呼び出します。 セッションが自動的にキャンセルされる 場合もあることに注意してください。 フォーカスが自然に 別の場所に移動した場合です。 SwiftUIのメニューバーエクストラは この作業の多くを代行してくれます。 WWDC26の「Use SwiftUI with AppKit and UIKit」 をご覧ください。 AppKitアプリでSwiftUIの メニューバーエクストラを使う方法が学べます。 アプリがマウスと同様に キーボードでも動作するよう確認することは Macを選ぶ多くのパワーユーザーに とって特に重要です。 シームレスな移行を提供することが アプリの内外で、ユーザーを サポートするもう一つの方法です。
シームレスな移行といえば 優れたMacアプリは シームレスに終了しすぐに復元します。 抵抗なく終了し 終了しなかったかのように 元の状態に戻ります。
ユーザーはいつでも アプリを終了できるべきです。 自分の意志で終了する場合もあれば システムの再起動が 必要な場合もあります。 夜間のソフトウェアアップデート中に 発生することがあります。 そのためアプリは本当に必要な場合に限り 終了をブロックするべきです。 アプリがシートを表示している場合 ウインドウが閉じられない場合があります。 ウインドウが閉じられないと アプリも終了できません。 NSWindowのプロパティ preventsApplicationTerminationWhenModalは デフォルトでtrueです。 それには正当な理由があります。 アプリがデータを失わないよう 確認することが重要です。 例えばドキュメントを 保存する必要がある場合などです。 このプロパティをfalseに設定します。 介入を厳密に必要としない すべてのモーダルやシートに対して アプリのより円滑な 終了を可能にします。
円滑な終了の処理が完了したら 次のステップは復元です。 NSWindowRestorationを使って アプリの復元方法をカスタマイズします。 状態の復元には3つのステップがあります。 状態復元へのオプトイン UIの状態のエンコード、そして ウインドウとUIを復元するためのデコードです。 NSWindowRestorationを使った コードを説明します。 まずウインドウコントローラーで ウインドウの識別子を設定します。 メインウインドウや設定ウインドウなど 共通のウインドウには autosave名を設定します。 ウインドウを同じフレームで アクティブなスペースに復元するのに役立ちます。 ドキュメントウインドウには autosave名を設定する必要はありません。 次にwindow.isRestorableを trueに設定します。 AppKitがウインドウに対して encodeRestorableStateとrestoreStateを呼び出せるよう。 これによりAppKitはウインドウの状態を 自動的に復元できます。 どのウインドウが最小化されていたか どれが最前面にあったか フルスクリーンだったかなどです。 また、window.restorationClassも設定します。 アプリが再起動されたときに 呼び出されるものです。 ウインドウ自体を復元するためです。 encodeRestorableStateを使って 必要なすべてを保存します。 ウインドウの状態を再現するために必要なものです。 スーパーの実装も呼び出して 状態が正しく復元されるようにします。 この例では 選択されたアイテムの識別子が productIdentifierキーでエンコードされます。
ドキュメントやデータベースに存在する データのエンコードは避けてください。 状態復元の目的はUIの状態を 再現できるようにすることです。 アプリ全体を再シリアライズすることでは ありません。 すべてのNSResponderにはencodeRestorable_ Stateメソッドがあり、オーバーライドできます。 ビューの状態も同様に管理します。
.encodeRestorableStateはオブジェクトの 状態が無効化されたときのみ呼ばれます。 ビュー階層に変更があるたびに 保存された状態を変更すべき場合は .invalidateRestorableState( )を呼び出します。 この例では サイドバーで別の製品が選択されると このメソッドが呼び出されます。 後ほど 無効化されたすべてのものに対して encodeRestorableStateが呼ばれます。
これで終了前のUIの状態を 保存できます。 アプリが再起動されると UIを復元するために すべての情報をデコードする必要があります。 まずウインドウを復元してから それらのウインドウの 状態を復元します。 ウインドウ復元クラスで restoreWindow withIdentifierメソッドを 実装します。 アプリのウインドウを 再作成するためです。 このメソッドは復元される すべてのウインドウに対して呼び出されます。 パラメータにはウインドウの 識別子が含まれます。 対応するウインドウとともに呼び出す必要がある completionHandlerも含まれます。 識別子を使ってウインドウコントローラーと ウインドウを再作成します。 .mainWindowはアプリデリゲートの .mainWindowControllerですでに利用可能です。 既存の.windowとともに completionHandlerを呼び出します。 その他のウインドウには ウインドウコントローラーをインスタンス化して .windowをcompletionHandlerに 渡します。 ウインドウの作成に失敗した場合も エラーとともにcompletionHandlerを呼び出します。 AppKitはすべての復元可能ウインドウを 待機するため、必ずcompletionHandlerを呼び出します。 このメソッド内から 呼び出せない場合は ハンドラを保存して 後で呼び出します。 必ず呼び出すようにしてください。 ウインドウが復元されたら 最後のステップは各ウインドウの UIを復元することです。 ウインドウコントローラーの restoreStateメソッドで AppKitは以前エンコードしたキーを含む 同じcoderオブジェクトを渡してくれます。 ここでアプリの状態を再現するために 必要なデータを取得します。 識別子をデコードして 対応するビューコントローラーに渡します。 完了すると、ウインドウは 以前と同じ状態になります。 終了と再起動を中断なく 感じられるよう改善することで ユーザーが中断したところから すぐに再開できます。 アプリを終了した場合でも Macを再起動した場合でも。 状態復元を実際に学ぶには コードサンプル「Restoring your app's state with AppKit」をご覧ください。
入力と復元を理解したら アプリとMacが出会う もう一つの領域、UIがあります。 macOS 26で導入されたLiquid Glassは 進化し続けています。 多くのアップデートは 自動的にアプリに適用されます。
macOS 26でLiquid Glassを採用した場合 macOS 27で実行すると いくつかの変更が自動的に適用されます。 NSScrollEdgeEffectStyleの自動設定は ハードエッジエフェクトになります。 タイトルバーのウインドウタイトルなど フリーフローティングテキストがある場合です。
サイドバーはウインドウの端まで延びます。 サイドバーの選択はセミボールドの テキストスタイルで強調されます。
コンテンツは引き続きサイドバーの 背後に流れます。
サイドバー上の枠付きツールバーアイテムも Liquid Glassを採用します。 macOS 27の新機能として glassに追加できるエフェクトがあります。 クリックするとglassが微妙に弾んで コントロールがインタラクションに 応答している感覚を与えます。 マップはカスタムコントロールの いくつかにこれを使用しています。
このエフェクトはアプリ内の すべてのglassに適用するものではありません。 コントロールやボタンに使用します。 またはインタラクティブなコントロールの glassコンテナに使用します。 少しでも大きな効果があります。
丸みのある長方形はAppleエコシステムの 特徴として何十年も続いてきました。 ハードウェアのベゼルからmacOS全体の コントロールやコンテナまでです。
AppKitには同心円のための新しいAPIがあります。 コーナーに配置されたコンテンツは コンテナの形状に適応できます。 ウインドウの他の部分と 不調和に感じることがなくなります。 例えば、マップのローカルWeatherビューは ウインドウと同心です。
ビューがコンテナの コーナー近くにある場合 そのコーナーはコンテナの カーブに合わせる必要があります。 ビューがコンテナのコーナーに近いほど 半径を合わせる必要があります。
ボタンやビューを同心にするには cornerConfiguration APIを使用します。 まずカスタムビューサブクラスを作成します。 カスタムビューで cornerConfigurationをオーバーライドして NSViewCornerConfiguration?を返します。
半径にはNSViewCornerRadiusの .containerConcentricを使用します。 コンテナビューに基づいて 半径を計算します。 最小値も設定します。 すべてのコーナーが常に 丸みを帯びるようにするためです。
設定にはさまざまな種類の ファクトリーメソッドから選択できます。 4つのコーナーすべてで 同じ半径のroundedRectを維持するには .uniformCornersを使用します。
以上がモダンなmacOSで アプリを調和させるためのヒントです。 始めるにあたっての 簡単なまとめをお伝えします。 アプリ内でmouseDownをオーバーライドしている 箇所を特定して 代わりにビューAPI、コントロールイベント またはGesture Recognizerを使用します。 トラッキングループよりも ユーザーの意図を優先します。 アプリがマウスと同様に キーボードでも動作することを確認します。 終了と再起動をシームレスに感じられるようにして ユーザーが中断したところから 正確に再開できるようにします。 ビュー階層を評価して ビューとボタンに同心性を採用します。
ご視聴ありがとうございました。 コンピューターの使い方を学ぶ学生向けのアプリから 世界で最も重要なツールや芸術を 作るパワーユーザー向けのアプリまで 皆さんのアプリはこの体験において 中心的な役割を果たしています。 これからも創り続けてください。
-
-
3:35 - Modern dragging delegate
// Modern dragging delegate methods func tableView(_ tableView: NSTableView, pasteboardWriterForRow row: Int) -> (any NSPasteboardWriting)? { let pasteboardItem = NSPasteboardItem() pasteboardItem.setString(..., forType: .string) return pasteboardItem } -
4:55 - Control events
// Use control events let button = NSButton() button.addTarget( self, action: #selector(trackingEndedOutsideHandler), for: .trackingEndedOutside ) -
5:44 - hitTest override
override func hitTest(_ point: NSPoint) -> NSView? { return nil } -
6:24 - autorecalculatesKeyViewLoop
window.autorecalculatesKeyViewLoop = true -
7:37 - Expanded interface delegate — setup
// Set the expanded interface delegate @main class LightAppDelegate: NSObject, NSApplicationDelegate { lazy var lightStatusItem: NSStatusItem = { ... }() func applicationDidFinishLaunching(_ notification: Notification) { // ... lightStatusItem.expandedInterfaceDelegate = self } } -
7:52 - Expanded interface delegate — methods
// Implement the delegate methods extension LightAppDelegate: NSStatusItemExpandedInterfaceDelegate { // ... func statusItem(_ statusItem: NSStatusItem, didBegin session: NSStatusItemExpandedInterfaceSession) { // Show window } func statusItemDidEndExpandedInterfaceSession( _ statusItem: NSStatusItem, animated: Bool) { // Hide window } func selectedAction() { // Take the action // Cancel session to request window dismissal lightStatusItem.expandedInterfaceSession?.cancel() } } -
8:16 - Expanded interface delegate — cancel
// Cancel the session when dismissing extension LightAppDelegate: NSStatusItemExpandedInterfaceDelegate { // ... func statusItem(_ statusItem: NSStatusItem, didBegin session: NSStatusItemExpandedInterfaceSession) { // Show window } func statusItemDidEndExpandedInterfaceSession( _ statusItem: NSStatusItem, animated: Bool) { // Hide window } func selectedAction() { // Take the action // Cancel session to request window dismissal lightStatusItem.expandedInterfaceSession?.cancel() } } -
9:45 - preventsApplicationTerminationWhenModal
window.preventsApplicationTerminationWhenModal = false -
10:18 - Set window identifiers for state restoration
// Set window identifiers for state restoration @MainActor class MainWindowController: NSWindowController, NSWindowDelegate { // ... convenience init() { let window = NSWindow( ... ) // ... window.identifier = NSUserInterfaceItemIdentifier(WindowIdentifiers.mainWindow) window.setFrameAutosaveName(WindowIdentifiers.mainWindow) window.isRestorable = true window.restorationClass = WindowRestorationHandler.self // ... } } -
11:04 - encodeRestorableState
// Preserve state to recreate the UI @MainActor class MainWindowController: NSWindowController, NSWindowDelegate { // ... override func encodeRestorableState(with coder: NSCoder) { super.encodeRestorableState(with: coder) // ... coder.encode(selectedProduct?.identifier.uuid, forKey: RestorationKeys.productIdentifier) // ... } // ... } -
11:50 - invalidateRestorableState
// Invalidate restorable state when the view hierarchy changes @MainActor class MainWindowController: NSWindowController, NSWindowDelegate { // ... convenience init() { // ... splitViewController.onProductSelected = { [weak self] product in self?.invalidateRestorableState() } } } -
12:26 - restoreWindow(withIdentifier:)
// Restore windows class WindowRestorationHandler: NSObject, NSWindowRestoration { static func restoreWindow( withIdentifier identifier: NSUserInterfaceItemIdentifier, state: NSCoder, completionHandler: @escaping (NSWindow?, Error?) -> Void ) { //... if identifier == .mainWindow, let window = appDelegate.mainWindowController?.window { completionHandler(window, nil) } else if identifier == .imageWindow { let controller = ImageWindowController() appDelegate.imageWindowControllers.append(controller) completionHandler(controller.window, nil) } else { completionHandler(nil, error) } } } -
13:29 - restoreState
// Restore window UI @MainActor class MainWindowController: NSWindowController, NSWindowDelegate { //... override func restoreState(with coder: NSCoder) { super.restoreState(with: coder) if let productId = coder.decodeObject( of: [NSString.self], forKey: RestorationKeys.productIdentifier) as? String { splitViewController?.selectedProductId = productId } //... } } -
16:11 - cornerConfiguration
// Subclass NSView to override cornerConfiguration class LocalWeatherView: NSView { // ... override var cornerConfiguration: NSViewCornerConfiguration? { let radius: NSViewCornerRadius = .containerConcentric(minimumCornerRadius) return .uniformCorners(radius: radius) } // ... }
-
-
- 0:00 - Introduction
A modern app takes advantage of how AppKit interfaces with Mac so that its form and function feel in harmony with the rest of the system. That harmony shows up in precision input, continuity across launches, and look and feel.
- 1:06 - Modern input
Precision input devices have been at the heart of the Mac since the very beginning. Learn modern APIs for handling mouse events, keyboard navigation, and status items.
- 1:27 - Modern event handling with gesture recognizers
Gesture recognizers are the modern way to handle mouse events in AppKit. mouseDown: overrides and tracking loops must be replaced with modern APIs.
- 2:25 - Selection, context menus, and drag and drop
AppKit has dedicated view-based APIs for the most common mouseDown: use cases: observe selected on collection and table types for selection, use menuForEvent: or .menu for context menus, and use modern pasteboard delegate methods for drag and drop.
- 3:52 - Text selection in custom views
NSTextSelectionManager brings classic macOS text selection behaviors to any view outside of NSTextView. Attach it to a view to get bidirectional selection, drag and drop, and toggling.
- 4:26 - Control events and gesture recognizers
Control events let you react to user-driven tracking state changes on standard controls. For custom interactions, use NSGestureRecognizer.
- 5:51 - Keyboard navigation and status items
Enable autorecalculatesKeyViewLoop on your window to keep Tab navigation correct as views change. For status items with custom UI, use the expanded interface session API so AppKit can manage keyboard focus correctly.
- 8:57 - Continuity across launches
A great Mac app seamlessly quits and quickly restores. Learn how to handle graceful app termination and state restoration using NSWindowRestoration.
- 9:08 - Graceful app termination
Apps should quit without blocking, especially during system reboots. Set preventsApplicationTerminationWhenModal to false on any sheet or modal that does not strictly require user intervention.
- 9:55 - State restoration
Use NSWindowRestoration to save and recover your app's UI across launches, so it picks up exactly where people left off.
- 14:09 - Design updates
There's one more area where your app and the Mac meet: the UI. Learn about Liquid Glass updates in macOS 27 and the new concentricity API.
- 14:24 - Liquid Glass updates in macOS 27
Liquid Glass continues to evolve in macOS 27, and many updates apply automatically. Sidebars, scroll edge effects, and toolbar items all receive refinements, and a new interactive glass effect gives controls a physical sense of response when clicked.
- 15:41 - Concentricity
The NSViewCornerConfiguration API lets views near a container's corners automatically match the container's corner radius using .containerConcentric, so views adapt to the shape of their container instead of feeling at odds with the rest of the window.
- 16:59 - Next steps
Prioritize gesture recognizers and view-based APIs over mouseDown:, ensure your app is fully keyboard-navigable, make quit and relaunch feel seamless, and adopt concentricity in your view hierarchies.