28 октября Apple показала MacBook Pro с PROLED XDR Display — первый Mac с сенсорным экраном, на macOS 27 Golden Gate. Для разработчиков в релизе есть одна главная фраза: многие приложения заработают с касаниями сразу, потому что macOS автоматически переводит касания в клики. Для кнопок, таблиц и SwiftUI-жестов это правда. Собственный view с трекинг-циклом в mouseDown(with:) получит эмулированную мышь: клик, прокрутку, правую кнопку по долгому нажатию. Самого пальца он не увидит, и отменять жест, когда его перехватила система, ему будет нечем.
Apple опубликовала три Tech Talk и техзаметку TN3212. Ниже — что всё это значит для кода, если вы пишете под сенсорный экран MacBook Pro, и с чего начинать. Сразу оговорка, чтобы не путать два анонса: 14-дюймовый MacBook Pro с M6, вышедший 27 октября, без касаний. Тач есть только у моделей с PROLED XDR Display на M5 Pro и M5 Max, 14 и 16 дюймов. Предзаказ открыт с 28 октября, поставки с 6 ноября.
Сенсорный экран MacBook Pro: что заработает без правок#
Apple называет «touch native» кнопки, контролы, таблицы, коллекции, split view, вкладки и скролл: AppKit строит их на gesture recognizers, и для этих компонентов касание работает так же, как клик. Существующие жесты SwiftUI и Mac Catalyst, по словам Apple, «просто работают».
Остальное закрывает эмуляция мыши. TN3212 описывает её для сторонних UI-фреймворков и самописных mouseDown(with:):
| Касание | Что получит приложение |
|---|---|
| Тап | mouseDown, затем mouseUp |
| Касание с сразу начатым движением | события прокрутки, как с трекпада |
| Долгое нажатие без движения | rightMouseDown, при движении rightMouseDragged, на отпускании rightMouseUp |
| Щипок или поворот двумя пальцами | magnify и rotate одновременно |
В докладе добавлено: один палец прокручивает с инерцией, перетаскивание начинается с небольшой задержкой. Ещё одна деталь важна для дизайна: интерфейс macOS на машине с тачем не меняется. Размеры окон, текста и контролов те же, касание остаётся вторичным вводом.
Catalyst-приложения отличают мышь от касания по UITouch.touchType и UIGestureRecognizer.allowedTouchTypes — API старые. В macOS 27 исправлена одна поломка: вью с UIDragInteraction и UIContextMenuInteraction теперь могут срабатывать оба от касания.
Прямое касание приходит только в gesture recognizers#
Цитата из доклада Рэйли Ледета: «Gesture recognizers are the only way to get direct touches on macOS». Следствия из TN3212: локальные и глобальные event monitors касаний не получают, NSApp и sendEvent(_:) их тоже не видят, по responder chain они не идут. Клавиатура и колесо прокрутки ходят по цепочке как раньше.
Apple показала минимальный способ принять касание в кастомном view — pan-распознаватель только для пальца:
let touchRecognizer = NSPanGestureRecognizer(target: self, action: #selector(touchPan))
touchRecognizer.buttonMask = 0x0
touchRecognizer.allowedTouchTypes = .direct
wiringPanelView.addGestureRecognizer(touchRecognizer)allowedTouchTypes с типом NSTouch.TouchTypeMask существует с macOS 10.12.2. Новое в другом: иначе касание до кода не доберётся.
Пара ловушек из TN3212. Распознаватели добавляются только к view; чтобы ловить касания в контроллере, вешайте их на его view. AppKit собирает кандидатов, поднимаясь вверх от самого верхнего view под пальцем, поэтому прозрачный перекрывающий view блокирует распознаватели, в том числе внутри стандартных контролов. Лечится переопределением hitTest(_:) с возвратом nil.
Трекинг-циклы можно оставить рядом: тач-распознаватель и старый mouseDown работают параллельно. Apple называет это «the fastest and least risky path for a dot release». Для основного релиза рекомендуют разделить трекинг на функции start, continue, end и cancel и вызывать их из обоих путей.
Замените трекинг в mouseDown на события NSControl#
Для своих кнопок и ползунков Apple предлагает вместо цикла в mouseDown подписываться на NSControl.Events через addTarget(_:action:for:). Пример из доклада:
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)Третья строка — та, которой не было в мире мыши. trackingCancelled приходит, когда трекинг прервал системный жест вроде Exposé или прокрутка; в этом обработчике откатывайте незавершённые изменения. Есть ещё valueChanged, primaryActionTriggered, menuActionTriggered и агрегаты allTrackingEvents, allEvents.
Три предупреждения. Если унаследоваться от стандартного контрола и переопределить любой left-mouse метод, он откатится к трекинг-циклу; то же для NSTableView и NSCollectionView, для которых Apple советует делегатные методы. После миграции NSApplication.currentEvent не возвращает событие, вызвавшее действие: читайте NSEvent.modifierFlags и NSEvent.pressedMouseButtons, но касания в pressedMouseButtons не отражаются. И по датировке: TN3212 называет trackingRepeated, valueChanged, primaryActionTriggered, menuActionTriggered и applicationReserved новыми в macOS 27, тогда как сам NSControl.Events в документации значится с macOS 11. Проверяйте доступность по каждому члену.
Несколько жестов сразу: исключительность и Info.plist#
По умолчанию активные распознаватели может иметь только одна иерархия view. Так AppKit воспроизводит модальный трекинг-цикл; исключение — прокрутка. Для панелей, где пальцы двигают несколько элементов одновременно (Apple приводит два слайдера в Logic), поведение отключается:
stoveControlsView.exclusiveGestureBehavior = .notExclusiveNSView.exclusiveGestureBehavior (macOS 27.0) принимает .inherit по умолчанию, .exclusive и .notExclusive; подвью наследуют значение. Стандартная панель цвета уже позволяет двигать несколько слайдеров сразу. Три ключа Info.plist, все с macOS 27.0:
| Ключ | Тип | Что делает |
|---|---|---|
NSViewGestureRecognizerIsExclusive | Boolean, по умолчанию YES | общий режим приложения; NO разрешает активные распознаватели в нескольких view |
NSGestureRecognizerSuppressesMainMenuActions | Boolean | игнорировать ли клавиатурные шорткаты меню, пока идёт неисключительный жест; эксклюзивные жесты подавляют их всегда |
NSIsTouchNative | Boolean | отключает встраиваемую AppKit эмуляцию мыши для приложений с не-AppKit UI; Apple: «Most AppKit apps don't need this key» |
Если на экране нет двух одновременных жестов, всё это трогать не нужно.
Зона касания 36 pt вместо 44 pt#
В дизайн-докладе Кас Лемменс называет число: «Touch on macOS uses a touch target of 36 points», то есть та же физическая величина на дисплее, что 44 pt на iOS. Фреймворковые контролы получают невидимую зону касания 36 pt и сохраняют визуальный размер. При этом опубликованный HIG на странице Buttons по-прежнему требует область не меньше 44×44 pt. Число 36 pt пока есть только в Tech Talk и в комментарии к коду Apple.
Для собственных контролов расширяйте зону касания, а не сам контрол, и делайте это только на пути касания: в примере Apple функция с expandHitRect вызывается из тач-распознавателя, поэтому мышь остаётся точной. Строки FormStyle по умолчанию 36 pt; в инспекторах остаются medium-контролы; small и mini тоже «fully support touch».
Hover касанием не заменить: у прямого касания нет аналога наведения. Тултипы работать с тачем не обязаны, кнопки прокрутки, появляющиеся при наведении, при касании скрыты. Всё, что важно и открывается только по hover, нужно продублировать, например в контекстном меню. Вариант Apple для AppKit: тач-распознаватель клика, временно показывающий элемент по тапу.
Скролл-вью: два пальца для рисования#
В macOS 27 у NSScrollView появились isTouchScrollingEnabled (по умолчанию true), minimumNumberOfTouchesForScrolling (по умолчанию 1), maximumNumberOfTouchesForScrolling и scrollGestureForRelationships для отношений отказа и исключения со скроллом. Для холста, где палец рисует, а прокрутка нужна тоже, пример Apple такой:
scrollView.minimumNumberOfTouchesForScrolling = 2Один палец рисует, два листают. Добавлен и NSRefreshController для pull-to-refresh: подключается через NSScrollView.refreshController, у него target/action, beginRefreshing() и endRefreshing(). Свой распознаватель, который не должен конкурировать со скроллом, можно пометить isCancellableByScrollGesture; по умолчанию false.
В 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 называет его новым, поэтому «доступен» корректнее, чем «новый».
Системные жесты забирают пальцы#
Жесты на дисплее, перечисленные в дизайн-докладе: три пальца вверх — Mission Control, вниз — App Exposé, три пальца влево и вправо — переключение Spaces, щипок большим пальцем и тремя — приложения, разведение — рабочий стол. Свайп вниз от верхней грани показывает скрытое меню, вверх от нижней — Dock, влево от правой — Центр уведомлений, вниз от правого верха — Пункт управления. Свайп поверх Dynamic Island вызывает Siri. Двойной тап по окну разворачивает его на весь экран, флик раскладывает по тайлам.
Практически важное: если одновременно двигать три слайдера и больше в одну сторону, сработает системный жест. Для редких окон, где системные жесты мешают, в примерах Apple показаны три отключающих API: NSWindow.disablesDirectTouchSystemGestures, UIViewController.prefersDirectTouchSystemGesturesDisabled для Catalyst и View.directTouchSystemGesturesDisabled() для SwiftUI. Они показаны в sample code, но страниц документации у них пока нет. Рекомендация Apple: отключать «only if the interaction capabilities they open up are critical for the window», и лучше только на критичный момент.
Дисплей и Dynamic Island: коротко#
Адаптивная частота ProMotion 10–120 Гц, яркость до 1000 нит устойчиво на весь экран и до 2000 нит пиково (в SDR такой пик бывает только на улице, в HDR — на HDR-контенте). API для EDR не изменились: NSScreen.maximumExtendedDynamicRangeColorComponentValue, CAMetalLayer.wantsExtendedDynamicRangeContent работают по-прежнему. Нового API геометрии Dynamic Island для AppKit в документации нет, остаются safeAreaInsets и auxiliaryTopLeftArea. Про выгорание: Apple говорит только, что движок дисплея компенсирует износ пикселей, рекомендаций разработчикам нет.
Live Activities на Mac Apple в Tech Talk описывает так: универсальные приложения с существующими активностями заработают без изменений, нативные macOS-приложения смогут создавать их напрямую. На Mac с Dynamic Island они у верхней грани, на остальных в строке меню. Но документация ActivityKit пока не называет macOS среди поддерживаемых платформ, а alertConfiguration: .announcement(...) из примера Apple отсутствует в списке членов AlertConfiguration. Закладываться на это стоит после выхода документации.
Как тестировать без нового Mac#
Apple: с последней беткой macOS и инструментами разработчика все возможности тача доступны на iPad, подключённом через Sidecar. На macOS 27 с iPad на iPadOS 27 касания тоже приходят только через gesture recognizers.
Осторожно с NSScreen.touchCapabilities (.multiTouch): при подключённом Sidecar-дисплее с тачем он возвращает true для всех экранов. Apple советует не ветвиться по этому значению, а обрабатывать ввод независимо от источника.
Две опции для отладки (user defaults): NSCrashOnStuckGestureTimeout роняет приложение, когда зависший эксклюзивный жест отменяется автоматически через несколько секунд; NSGestureRecognizerCrashOnMissingOverrides отмечает подклассы, где нет обязательных переопределений. Ещё одна ловушка, которая бьёт при пересборке: в приложении, собранном с macOS 27 SDK, базовая NSGestureRecognizer.location(in:) вернёт NSZeroPoint и залогирует ошибку, если подкласс её не переопределил. Кастомные распознаватели должны доходить до .ended, .failed или .cancelled, иначе блокируют остальные; реализуйте mouseCancelled(with:) и touchesCancelled(with:).
Все новые символы имеют macOS 27.0, поэтому при поддержке старых систем оборачивайте вызовы в #available(macOS 27, *).
Список на первую неделю#
- Пересоберите приложение под macOS 27 SDK и пройдите логи: найдите подклассы
NSGestureRecognizerбезlocation(in:). - Включите
NSCrashOnStuckGestureTimeoutиNSGestureRecognizerCrashOnMissingOverridesв debug-схеме. - Подключите iPad через Sidecar и пройдите основные экраны пальцем: тап, перетаскивание, долгое нажатие, скролл.
- Найдите кастомные
mouseDownс трекингом и добавьте рядом тач-распознавательallowedTouchTypes = .direct. - Замените трекинг своих контролов на
NSControl.Events, обработайтеtrackingCancelled. - Расширьте зоны касания мелких кастомных контролов до 36 pt только в тач-пути.
- Найдите действия, доступные только по 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 нет страницы про touch on Mac, а страница Gestures всё ещё говорит, что пользователи Mac полагаются на клавиатуру, мышь и трекпад. API отключения системных жестов не задокументированы, доступность Live Activities для нативных macOS-приложений в документации не отражена. Apple называет некий «gesture modernization skill» в инструментах разработчика, найти его не удалось. Вопросы можно задать на групповых лабораториях 18 ноября (9–10 утра PT) и 19 ноября (6–7 вечера PT).
На Hacker News спорят в основном об отпечатках пальцев, усталости руки и отсутствии режима планшета. Мобильные разработчики, наоборот, рады проверять iOS-жесты прямо на экране Mac. Для кода из этих споров следует то же, что Apple говорит в дизайн-докладе: касание остаётся вторичным вводом, и каждый жест нужен ещё на трекпаде и в меню.
Если вы дорабатываете интерфейс под новые материалы, полезно сверить его с разбором Liquid Glass в SwiftUI. Похожая история адаптации под новое железо описана в руководстве по iPhone Duo, а меню-бар приложениям пригодится заметка про macOS menu bar app на SwiftUI. Первоисточники: Touch on Mac, TN3212, Tech Talks Design for touch on Mac, Bring Touch to your Mac app и Bring Live Activities to Mac.



