10月28日、AppleはPROLED XDR Display搭載のMacBook Proを発表しました。macOS 27 Golden Gateで動く、初のタッチスクリーンMacです。macOS タッチスクリーン対応について、発表にある開発者向けの一文は、macOSがタッチを自動的にクリックへ変換するため、多くの既存アプリはそのままTouchで動く、というものです。ボタンやテーブル、SwiftUIのジェスチャーについてはその通りです。一方、mouseDown(with:) の中でトラッキングループを回している独自ビューでは半分だけ正しい話になります。そのビューに届くのはエミュレートされたマウスです。クリック、スクロール、長押しによる右クリックは来ますが、指そのものは見えません。システムがジェスチャーを引き取ったときに、操作を取り消す手段もありません。
AppleはTech Talkを3本とテクニカルノートTN3212を公開しました。この記事では、macOS タッチスクリーン対応としてコードに何が必要か、どこから手を付けるかをまとめます。混同しやすいので先に書いておくと、10月27日に発表された14インチのM6搭載MacBook Proにはタッチがありません。Touchが使えるのは、M5 ProまたはM5 Maxを積む14インチと16インチのPROLED XDR Displayモデルで、予約は10月28日から、出荷は11月6日からです。
macOS タッチスクリーン:変更なしで動くもの#
Appleはボタン、コントロール、テーブル、コレクション、スプリットビュー、タブ、スクロールを「touch native」と呼んでいます。これらはAppKitがジェスチャーレコグナイザーで構築しているため、指はクリックと同じように働きます。既存のSwiftUIとMac Catalystのジェスチャーも、トークによれば「just work」です。
それ以外はマウスエミュレーションが受け持ちます。TN3212は、サードパーティのUIフレームワークや手書きの mouseDown(with:) 向けに次のように説明しています。
| タッチ | アプリに届くもの |
|---|---|
| タップ | mouseDown、続けて mouseUp |
| 触れてすぐ動かすタッチ | トラックパッドのスクロールイベント |
| 動かない長押し | rightMouseDown、動けば rightMouseDragged、離すと rightMouseUp |
| 2本指のピンチまたは回転 | トラックパッドのmagnifyとrotateが同時に |
トークでは、1本指で慣性付きスクロール、少し押したままにするとドラッグが始まる、とも説明されています。デザイン面で大事な点が一つあります。タッチ対応機でもmacOSの見た目は変わりません。ウィンドウ、文字、コントロールのサイズはそのままで、タッチはあくまで補助的な入力です。
Catalystアプリは、従来の UITouch.touchType と UIGestureRecognizer.allowedTouchTypes でマウスとタッチを見分けます。macOS 27ではCatalystで足りなかった点も一つ埋まり、UIDragInteraction と UIContextMenuInteraction の両方を持つビューで、タッチでも両方が起動できるようになりました。
直接のタッチはジェスチャーレコグナイザーにしか届かない#
AppKitエンジニアのRaleigh Ledetの言葉です。"Gesture recognizers are the only way to get direct touches on macOS." TN3212はその帰結を具体的に書いています。ローカルとグローバルのイベントモニターはタッチを受け取らず、タッチは NSApp や sendEvent(_:) を通らず、レスポンダチェーンにも乗りません。キーボードとスクロールホイールは従来どおりチェーンを通ります。
Apple自身の最小例は、指だけを受け付けるパンレコグナイザーです。
let touchRecognizer = NSPanGestureRecognizer(target: self, action: #selector(touchPan))
touchRecognizer.buttonMask = 0x0
touchRecognizer.allowedTouchTypes = .direct
wiringPanelView.addGestureRecognizer(touchRecognizer)allowedTouchTypes はmacOS 10.12.2からある NSTouch.TouchTypeMask で、新しくはありません。新しいのは、これが唯一の入り口になったことです。
TN3212から罠を二つ。レコグナイザーはビューにしか付けられないので、ビューコントローラでタッチを受けたい場合は、その view に付けます。また、AppKitはヒットテストで見つかった最前面のビューから上にたどって候補を集めるため、透明で重なったビューがあると、標準コントロール内部のものも含めてレコグナイザーが塞がれます。その透明ビューで hitTest(_:) をオーバーライドし、nil を返してください。
既存のトラッキングループはそのまま残し、横にタッチ専用レコグナイザーを足せます。Appleはこれを「the fastest and least risky path for a dot release」と呼んでいます。大きなリリースでは、トラッキングをstart、continue、end、cancelの関数に分け、両方の経路から呼ぶ構成を勧めています。
mouseDownのトラッキングをNSControlのイベントに置き換える#
自作のボタンやスライダーには、mouseDown でループを回す代わりに、addTarget(_:action:for:) で NSControl.Events を購読する方法がAppleの推奨です。トークのサンプルです。
ignitionButton.addTarget(self, action: #selector(buttonDidStartTracking), for: [.trackingBegan, .trackingEntered])
ignitionButton.addTarget(self, action: #selector(buttonDidEndTracking), for: [.trackingExited, .trackingEndedInside])
ignitionButton.addTarget(self, action: #selector(buttonCancelledTracking), for: .trackingCancelled)3行目はマウスの世界に相当するものがありません。trackingCancelled は、Exposéなどのシステムジェスチャーやスクロールがトラッキングを中断したときに発火します。途中までの変更はここで元に戻します。ほかに valueChanged、primaryActionTriggered、menuActionTriggered、まとめ用の allTrackingEvents と allEvents があります。
注意が三つ。標準コントロールを継承して左マウス系のレスポンダメソッドを一つでもオーバーライドすると、トラッキングループにフォールバックします。NSTableView と NSCollectionView も同様で、Appleはデリゲートメソッドの利用を案内しています。移行後は NSApplication.currentEvent がアクションを起こしたイベントを返さなくなるため、NSEvent.modifierFlags と NSEvent.pressedMouseButtons を読みますが、タッチは pressedMouseButtons に現れません。最後に、時期の記述が少し食い違っています。TN3212は trackingRepeated、valueChanged、primaryActionTriggered、menuActionTriggered、applicationReserved をmacOS 27の新規としていますが、ドキュメント上の NSControl.Events 自体はmacOS 11です。メンバー単位で利用可否を確認してください。
複数のジェスチャーを同時に動かす#
既定では、アクティブなレコグナイザーを持てるビュー階層は一つだけです。これはモーダルなトラッキングループを模したもので、スクロールだけが例外です。指で複数の対象を同時に動かすパネル(デザインのトークではLogicの2本のスライダーが例)では、これを解除します。
stoveControlsView.exclusiveGestureBehavior = .notExclusiveNSView.exclusiveGestureBehavior(macOS 27.0)は、既定の .inherit、.exclusive、.notExclusive を取り、サブビューは値を継承します。AppKit標準のカラーパネルは、すでに複数のスライダーを同時に動かせます。Info.plistのキーは三つで、いずれもmacOS 27.0です。
| キー | 型 | 効果 |
|---|---|---|
NSViewGestureRecognizerIsExclusive | Boolean、既定 YES | アプリ全体の既定値。NO で複数ビューのレコグナイザーを同時に有効にできる |
NSGestureRecognizerSuppressesMainMenuActions | Boolean | 非排他ジェスチャー中にメニューのキーボードショートカットを無視するか。排他ジェスチャーは常に抑制する |
NSIsTouchNative | Boolean | AppKit以外のUIを組み込むアプリで、AppKitがマウスエミュレーション用レコグナイザーを差し込むのを止める。Apple: "Most AppKit apps don't need this key" |
アプリに同時ジェスチャーが必要な画面がなければ、ここには触れなくて構いません。
タッチターゲット:トークは36pt、HIGは44pt#
デザイナーのCas Lemmensは数字を挙げています。"Touch on macOS uses a touch target of 36 points." これはiOSの44ポイントと画面上で同じ物理サイズです。フレームワークのコントロールには見えない36ptのタッチターゲットが付き、見た目のサイズは変わりません。知っておくべき食い違いがあります。公開中のHIGのButtonsページは、いまも44x44pt以上のヒット領域を求めています。36ptはデザインのTech Talkと、Appleのサンプルコード内のコメントに由来し、ガイドラインには入っていません。
独自コントロールでは、コントロールそのものではなくタッチのヒット領域を広げ、しかもタッチの経路だけで行います。Appleのサンプルでは expandHitRect の処理がタッチ用レコグナイザーから呼ばれるので、マウスの精度は保たれます。FormStyle の行は既定で36pt、インスペクタはmediumのコントロールのまま、smallとminiも「fully support touch」です。
hoverにはタッチの対応物がありません。ツールチップはタッチで動く必要がなく、hoverで現れるスクロールボタンはタッチ中は隠れたままです。hoverでしか出ない重要な操作には、コンテキストメニューなどの別経路が必要です。AppKitでのAppleの案は、タッチ専用のクリックレコグナイザーで、タップした間だけ要素を表示する方法です。
スクロールビュー:描画キャンバスは2本指で#
macOS 27では NSScrollView に isTouchScrollingEnabled(既定 true)、minimumNumberOfTouchesForScrolling(既定1)、maximumNumberOfTouchesForScrolling、そしてスクロールとの失敗・排他関係を設定する scrollGestureForRelationships が加わります。1本指で描き、それでもスクロールは必要なキャンバスには、Appleのサンプルの1行が使えます。
scrollView.minimumNumberOfTouchesForScrolling = 2プルして更新するための NSRefreshController もあり、NSScrollView.refreshController で接続し、target/action、beginRefreshing()、endRefreshing() を持ちます。スクロールに譲りたい独自レコグナイザーは、既定で false の isCancellableByScrollGesture を設定できます。
SwiftUIでは入力元を明示する#
GestureInputKinds(macOS 27.0)はオプションセットで、.directTouch、.indirectTouch(トラックパッドのマルチタッチ、Siri Remote)、.pointer、.pencil、.all があります。inputKinds 付きのイニシャライザーは DragGesture、LongPressGesture、MagnifyGesture、RotateGesture、SpatialTapGesture、TapGesture などにあります。指で描き、マウスで選択するキャンバスの例です。
let draw = DragGesture(minimumDistance: 0, coordinateSpace: .local, inputKinds: .directTouch)
let select = DragGesture(minimumDistance: 4, coordinateSpace: .local, inputKinds: .pointer)独自の NSGestureRecognizer がすでにあるなら、書き直さずに NSGestureRecognizerRepresentable でSwiftUIに包めます。メソッドは makeNSGestureRecognizer(context:)、updateNSGestureRecognizer(_:context:)、handleNSGestureRecognizerAction(_:context:)、makeCoordinator(converter:) で、.gesture(_:) で付けます。ドキュメントではmacOS 26.0とされ、WWDC26は新しいと呼んでいるため、「利用可能」と書くのが無難です。
システムジェスチャーが先に指を取る#
デザインのトークが挙げる画面上のジェスチャーは次のとおりです。3本指で上にスワイプでMission Control、下でアプリケーションExposé、左右で操作スペースの切り替え。親指と3本指のピンチインでアプリ、広げるとデスクトップ。上端から下にスワイプで隠れたメニューバー、下端から上でDock、右端から左で通知センター、右上端から下でコントロールセンター。Dynamic Island上のスワイプはSiriを呼びます。ニュースルームによれば、ウィンドウをダブルタップすると画面いっぱいに広がり、フリックするとタイル表示になります。
実務上のポイントは、3つ以上のスライダーを同じ方向に同時に動かすと、システムジェスチャーが発動することです。それが困る稀なウィンドウのために、Appleのサンプルコードは三つの無効化APIを示しています。AppKitの NSWindow.disablesDirectTouchSystemGestures、Catalystの UIViewController.prefersDirectTouchSystemGesturesDisabled、SwiftUIの View.directTouchSystemGesturesDisabled() です。いずれもサンプルコードに登場するだけで、ドキュメントページはまだありません。Appleの助言は、"only if the interaction capabilities they open up are critical for the window" の場合だけ無効にし、できれば重要な瞬間に限る、というものです。
ディスプレイとDynamic Island:要点だけ#
ProMotionは10〜120Hzで適応的に動きます。輝度は全画面の持続で最大1000ニト、ピークで2000ニトです(SDRは屋外、XDRはHDRコンテンツ時)。EDRのAPIは変わらず、NSScreen.maximumExtendedDynamicRangeColorComponentValue や CAMetalLayer.wantsExtendedDynamicRangeContent はこれまでどおり使えます。AppKit向けのDynamic Island形状の新APIはドキュメントに見当たらず、既存の safeAreaInsets と auxiliaryTopLeftArea が残っています。焼き付きについて、Appleはディスプレイエンジンが画素の劣化を補正するとだけ述べ、開発者向けのガイダンスはありません。
MacのLive Activitiesについて、Tech Talkは、既存のLive Activitiesを持つユニバーサルアプリはコード変更なしで動き、ネイティブmacOSアプリは直接作成できると説明しています。Dynamic Island搭載のMacでは上端に、そのほかではメニューバーに表示されます。ただしActivityKitのドキュメントにはまだmacOSが載っておらず、Appleのサンプルにある alertConfiguration: .announcement(...) も AlertConfiguration のメンバー一覧にありません。これに依存するのは、ドキュメントが出てからにしてください。
新しいMacなしでテストする#
Appleによれば、最新のmacOSベータと開発者ツールがあれば、Sidecarで接続したiPadでTouchの機能がすべて使えます。macOS 27では、iPadOS 27のiPadからSidecar経由で届くタッチも、ジェスチャーレコグナイザーでしか受け取れません。
NSScreen.touchCapabilities(.multiTouch)には注意が必要です。タッチ対応のSidecarディスプレイが接続されていると、すべてのディスプレイで true を返します。Appleは、この値で分岐せず、入力元を問わず処理することを勧めています。
デバッグ用のdefaultsは二つ。NSCrashOnStuckGestureTimeout は、固まった排他ジェスチャーが入力終了の数秒後に自動キャンセルされたときにアプリをクラッシュさせ、NSGestureRecognizerCrashOnMissingOverrides は必要なオーバーライドが欠けたサブクラスを検出します。再ビルドで踏む罠もあります。macOS 27 SDKでビルドしたアプリでは、サブクラスがオーバーライドしていないと、基底の NSGestureRecognizer.location(in:) は NSZeroPoint を返し、エラーをログに出します。独自レコグナイザーは必ず .ended、.failed、.cancelled のいずれかに到達させてください。そうしないと他のレコグナイザーを塞ぎます。mouseCancelled(with:) と touchesCancelled(with:) を実装します。
ここで挙げた新シンボルはすべてmacOS 27.0です。古いOSもサポートするなら、呼び出しを #available(macOS 27, *) で包みます。
最初の1週間のチェックリスト#
- macOS 27 SDKで再ビルドし、
location(in:)をオーバーライドしていないNSGestureRecognizerのサブクラスを探す。 - デバッグスキームで
NSCrashOnStuckGestureTimeoutとNSGestureRecognizerCrashOnMissingOverridesを有効にする。 - iPadをSidecarで接続し、主要画面を指で操作する。タップ、ドラッグ、長押し、スクロール。
mouseDownでの独自トラッキングを探し、横にallowedTouchTypes = .directのタッチ専用レコグナイザーを足す。- 自作コントロールのトラッキングを
NSControl.Eventsに移し、trackingCancelledを処理する。 - 小さい独自コントロールのヒット領域を、タッチの経路だけで36ptまで広げる。
- hoverでしか使えない操作を探し、第二の経路を用意する。
- 同時ジェスチャーのあるウィンドウを確認し、
.notExclusiveが必要か判断する。
Appleがまだ説明していないこと#
developer.apple.com/touch-on-mac/ のハブは、発表当日の時点で未完成に見えます。「Documentation resources」と「Design resources」は「Coming soon」で、Xcode 27.3 betaも同じです。記事「Adopting gesture recognizers in your app」はHTML内でコメントアウトされ、404を返します。HIGにはMacのタッチのページがなく、Gesturesのページはいまも、Macのユーザーはキーボード、マウス、トラックパッドに頼ると書いています。システムジェスチャーの無効化APIはドキュメント化されておらず、ネイティブmacOSアプリでのLive Activitiesの提供状況もドキュメントに反映されていません。トークは開発者ツール内の「gesture modernization skill」に触れていますが、まだ見つかりません。質問は、11月18日(PT午前9〜10時)と11月19日(PT午後6〜7時)のグループラボへ。
Hacker Newsの反応は、指紋、腕の疲れ、タブレットモードの不在が中心で、モバイル開発者はiOSのジェスチャーをMacの画面で直接試せる点を評価しています。どれも上のリストを変えるものではありませんが、タッチは補助的で、ジェスチャーはトラックパッドやメニューでも使えるべきというAppleのデザイン方針とは合っています。
新しいマテリアルに合わせてUIを更新するなら、SwiftUIでのLiquid Glass対応も参照してください。新しいハードウェアへの適応という点で近い話はiPhone Duo対応ガイドにあり、メニューバーアプリならSwiftUIのmacOSメニューバーアプリの記事が役立ちます。一次情報はTouch on Mac、TN3212、Tech TalkのDesign for touch on Mac、Bring Touch to your Mac app、Bring Live Activities to Macです。



