Используйте кнопку SwiftUI Button, если ваша пользовательская поверхность на Mac выполняет именованное действие. Используйте жест щелчка, если взаимодействию нужны сведения о жесте, например координаты на холсте. В этом руководстве создаётся небольшой редактор фигур, чтобы показать, где уместен каждый вариант.
Apple прямо рекомендует использовать кнопку, когда элемент выполняет её функцию. В качестве метки кнопки можно использовать любое представление, поэтому для создания индивидуального внешнего вида не требуется заменять стандартное поведение взаимодействия. SwiftUI предоставляет стандартные механизмы поведения кнопок, включая метки и подсказки для универсального доступа. Жесты
Какое взаимодействие нужно вашей поверхности?
Разработчику приложения для Mac полезно спросить себя, какие данные должно передавать взаимодействие. Плитка с меткой Add circle выражает команду. Щелчок по пустой поверхности для рисования, чтобы разместить круг, выражает команду и координаты. Этот дополнительный ввод меняет выбор подходящей реализации.
В следующей таблице рекомендации Apple применяются к обычному настольному редактору фигур. Это рекомендации по реализации, основанные на API, а не правила для любого интерфейса.
| Задуманное пользователем взаимодействие | С чего начать | Причина |
|---|---|---|
Щелчок по пользовательской плитке с меткой Add circle |
Button с пользовательской меткой |
Поверхность запускает именованное действие. |
| Задать плитке особый фон и вид при нажатии | ButtonStyle |
Он позволяет изменить внешний вид, сохраняя стандартное поведение кнопки. |
| Щелчок по холсту для размещения круга в этой точке | SpatialTapGesture |
Результат содержит координаты взаимодействия. |
| Распознавание двух щелчков по представлению содержимого без необходимости получать координаты | onTapGesture(count: 2) |
Модификатор принимает требуемое число щелчков. |
| Настройка определённых кнопок мыши для взаимодействия AppKit | NSClickGestureRecognizer |
Он позволяет настроить кнопку мыши и число щелчков. |
Apple описывает кнопку как действие с меткой и отделяет настройку внешнего вида через ButtonStyle от настройки поведения взаимодействия через PrimitiveButtonStyle. В API жестов отдельно описаны число щелчков и пространственные результаты. Кнопка, onTapGesture(count:perform:), SpatialTapGesture, NSClickGestureRecognizer
Таким образом, пользовательская плитка действия изначально должна быть кнопкой. Используйте жест, когда можете определить, какие дополнительные данные или требования к распознаванию нужны поверхности.
Как создать пользовательскую кнопку, не реализуя её поведение заново?
Поместите команду в замыкание действия кнопки, а графику — в её метку. Используйте ButtonStyle для оформления. Согласно документации Apple, этот протокол сохраняет стандартное поведение взаимодействия платформы; PrimitiveButtonStyle служит отдельной точкой расширения для намеренного изменения поведения взаимодействия. ButtonStyle
В приведённом ниже примере две соседние поверхности: пользовательская кнопка Add circle и прямоугольный холст для размещения фигур. Обе вызывают одну и ту же функцию изменения состояния. Кнопка выбирает предсказуемую позицию; холст передаёт локальную точку. Благодаря этому различие видно без создания файлов, внешнего содержимого или второго приложения.
Создайте проект приложения для macOS с использованием SwiftUI и задайте целевую версию macOS 13 или новее — это минимальная версия, указанная в документации для SpatialTapGesture. Замените сгенерированные определения приложения и представления содержимого первым файлом, а второй файл добавьте в ту же цель. Оставьте ровно одно объявление приложения @main. Код проверен на предмет ошибок, но не компилировался и не запускался в среде с SDK macOS. SpatialTapGesture
Приложение целиком состоит из этих двух файлов. Сначала ShapeSurfaceApp.swift:
import SwiftUI
import Foundation
@main
struct ShapeSurfaceApp: App {
var body: some Scene {
WindowGroup {
ShapeSurfaceView()
}
}
}
private struct PlacedCircle: Identifiable {
let id = UUID()
let center: CGPoint
}
struct ShapeSurfaceView: View {
@State private var circles: [PlacedCircle] = []
private let limit = 6
private var isFull: Bool {
circles.count >= limit
}
var body: some View {
VStack(alignment: .leading, spacing: 16) {
Button {
let x = 35 + CGFloat(circles.count) * 50
addCircle(at: CGPoint(x: x, y: 100))
} label: {
HStack(spacing: 12) {
Circle()
.fill(Color.blue)
.frame(width: 24, height: 24)
Text("Add circle")
}
.frame(width: 280, height: 64)
.contentShape(RoundedRectangle(cornerRadius: 12))
}
.buttonStyle(SurfaceButtonStyle())
.keyboardShortcut("a", modifiers: [.command, .shift])
.disabled(isFull)
.opacity(isFull ? 0.4 : 1)
Text("Click the canvas to place a circle. Maximum: 6.")
ZStack(alignment: .topLeading) {
Color.gray.opacity(0.1)
ForEach(circles) { circle in
Circle()
.fill(Color.blue)
.frame(width: 24, height: 24)
.position(circle.center)
}
}
.frame(width: 320, height: 200)
.contentShape(Rectangle())
.gesture(
SpatialTapGesture()
.onEnded { value in
addCircle(at: value.location)
}
)
.disabled(isFull)
.border(Color.gray)
.accessibilityElement(children: .ignore)
.accessibilityLabel("Circle canvas")
.accessibilityValue("\(circles.count) of \(limit) circles")
Text(isFull
? "Six circles placed. Clear to start again."
: "\(circles.count) of \(limit) circles placed")
Button("Clear circles") {
circles.removeAll()
}
.disabled(circles.isEmpty)
}
.padding(24)
}
private func addCircle(at point: CGPoint) {
guard !isFull else { return }
// Keep each 24-point circle inside the 320-by-200 canvas.
let center = CGPoint(
x: min(max(point.x, 12), 308),
y: min(max(point.y, 12), 188)
)
circles.append(PlacedCircle(center: center))
}
}
Затем SurfaceButtonStyle.swift:
import SwiftUI
struct SurfaceButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.background(
RoundedRectangle(cornerRadius: 12)
.fill(Color.blue.opacity(
configuration.isPressed ? 0.25 : 0.1
))
)
}
}
Стиль читает configuration.isPressed, чтобы отобразить обратную связь; он сам не вызывает действие. Apple определяет это свойство как текущее состояние нажатия кнопки. Благодаря этому отрисовка и выполнение действия разделены. isPressed
Ограничение в шесть кругов — пример правила приложения. Обе точки входа используют addCircle(at:), которая проверяет доступность места перед добавлением. Неактивный вид сообщает о достижении лимита, а общая проверка гарантирует единообразие правила изменения состояния. Apple указывает, что disabled(_:) управляет взаимодействием с представлением, включая щелчки и жесты. disabled(_:)
Почему для холста используется пространственный жест щелчка?
Холсту нужны координаты щелчка пользователя. Базовая перегрузка onTapGesture(count:perform:) выполняет действие, но не передаёт координаты. SpatialTapGesture явно сообщает координаты, а его инициализатор по умолчанию задаёт одно взаимодействие в локальной системе координат. onTapGesture(count:perform:), SpatialTapGesture, init(count:coordinateSpace:)
В этой программе жест назначен холсту фиксированного размера, локальные координаты которого определяют положение кругов. Функция размещения ограничивает координаты центра так, чтобы от края оставалось 12 пунктов — столько же, как радиус круга. Поэтому щелчок рядом с краем размещает круг целиком внутри холста. Круги могут перекрываться; их количество остаётся видимым даже при перекрытии фигур.
У кнопки и холста намеренно различается поведение клавиатуры. Сочетание клавиш для кнопки выполняет то же действие, что и её активация: добавляет круг в следующую позицию. Оно не создаёт координаты указателя. Apple описывает сочетания клавиш как способ вызвать основное действие элемента управления и отмечает, что при совпадении назначений срабатывает первый найденный подходящий элемент. Назначьте это сочетание один раз в окне примера. keyboardShortcut(_:modifiers:)
Метка универсального доступа холста и счётчик описывают его содержимое, но не обеспечивают клавиатурный сценарий размещения по произвольным координатам. Кнопка позволяет добавить круг в заданную позицию без указателя. Если точное размещение также должно быть доступно с клавиатуры, добавьте явный интерфейс редактирования положения, а не полагайтесь на то, что жест или описательная метка его обеспечат.
Как согласовать область нажатия с пользовательской поверхностью?
Сначала задайте размер метки, затем определите нужную область взаимодействия. В примере метка кнопки имеет размер 280 на 64 пункта, а затем к ней применяется скруглённый прямоугольный contentShape. Холсту задаётся размер 320 на 200 пунктов, после чего указывается прямоугольная форма содержимого. Apple определяет contentShape(_:eoFill:) как форму, используемую для проверки нажатий. contentShape(_:eoFill:)
Так для видимых поверхностей задаются чёткие границы взаимодействия. Если при редизайне вы вынесете рамку за пределы метки кнопки, проверьте результат ещё раз: в этом примере размеры и форма содержимого связаны внутри метки. Продуманно задавайте область рисования, компоновки и взаимодействия вместо того, чтобы исправлять не реагирующий нажатиям отступ добавлением ещё одного обработчика.
Если поверхность перестала реагировать, проверьте следующее по порядку:
- Проверьте геометрию. Щёлкните по тексту метки, графике и пустой области внутри неё. Убедитесь, что форма содержимого описывает нужные границы.
- Проверьте отключённые родительские представления. Отключённый контейнер переопределяет состояние включённого дочернего элемента, поэтому локальный
.disabled(false)может не восстановить взаимодействие. disabled(_:) - Проверьте декоративные перекрытия. Если представление служит только для оформления, его можно исключить из проверки нажатий с помощью
.allowsHitTesting(false). Применяйте это к декору, а не к интерактивной поверхности. allowsHitTesting(_:) - Проверьте конкурирующие обработчики. Прежде чем менять приоритет жестов, определите, какое представление отвечает за действие.
Как избежать конкурирующих обработчиков и ошибок при распределении ответственности?
Используйте один обработчик для каждого предусмотренного способа активации. В примере пользовательская кнопка отвечает за команду, а холст — за размещение по координатам. Ни один из них не находится внутри родительского представления с дополнительным жестом размещения. Благодаря такой структуре легко понять, к чему приведёт один щелчок.
Не добавляйте второй обработчик касания к кнопке только ради вызова той же функции изменения состояния. Кроме того, не добавляйте жест к родительскому представлению с несколькими элементами управления, не решив, должны ли взаимодействия родителя и дочерних элементов выполняться одновременно. В примере Apple с simultaneousGesture показано, что могут срабатывать обработчики и родительского, и дочернего представления. highPriorityGesture вместо этого задаёт прикреплённому жесту более высокий приоритет. Эти модификаторы задают правила взаимодействия; они не взаимозаменяемы и не служат универсальными исправлениями. simultaneousGesture(_:including:), highPriorityGesture(_:including:)
Храните состояние приложения в представлении, которое им владеет. Здесь ShapeSurfaceView хранит данные о синтетических кругах, а стиль лишь отображает переданную метку. Apple рекомендует объявлять закрытое состояние на уровне самого высокого представления, которому нужен доступ к нему, и по необходимости передавать дочерним представлениям значения или привязки. State
Если позднее адаптер AppKit будет хранить обратный вызов, отдельно проверьте связи захвата. Замыкания Swift могут захватывать окружающие значения; если класс хранит замыкание, которое захватывает тот же экземпляр, может возникнуть сильная циклическая ссылка. Выбирайте список захвата с учётом реального срока жизни объектов. Замена кнопки жестом не решит проблему владения обратным вызовом. Замыкания
Когда стоит перейти к AppKit?
Рассмотрите AppKit, если вашей поверхности нужна документированная настройка взаимодействия с мышью. NSClickGestureRecognizer позволяет задать обязательное число щелчков и маску кнопок мыши. Apple указывает, что бит 0 соответствует основной кнопке, а бит 1 — вторичной; распознавание требует заданного числа щелчков без перетаскивания. Для настроенных кнопок мыши задержка событий также включается автоматически. NSClickGestureRecognizer, buttonMask
Выбирайте способ интеграции с учётом целевой версии ОС и компонента, которым вы владеете:
- В macOS 26 и новее
NSGestureRecognizerRepresentableинтегрирует распознаватель AppKit непосредственно в иерархию SwiftUI через.gesture(...). SwiftUI устанавливает целевой объект действия; реализуйте обработчик действия представляемого типа и используйте конвертер координат его контекста. Apple предупреждает, что геометрия связанного с распознавателем представления может отличаться от геометрии представления SwiftUI. NSGestureRecognizerRepresentable - Для представления AppKit, размещённого в SwiftUI,
NSViewRepresentableуправляет созданием, обновлением и удалением вашего представления. Используйте координатор для обратных вызовов, а компоновку представляемого представления оставьте под управлением SwiftUI. Этот API доступен начиная с macOS 10.15. NSViewRepresentable
При добавлении любого из этих адаптеров соблюдайте границы обработки событий. Согласно документации AppKit, распознаватель действует на связанном с ним представлении и его потомках, получает соответствующие события раньше представления и может задерживать их передачу. Также прямо указано, что он не поддерживает передачу отслеживания событий другим механизмам, например перетаскиванию или всплывающим меню. Создавайте такие взаимодействия с помощью предназначенных для них механизмов, а не считайте распознаватель щелчка универсальным перенаправителем событий. NSGestureRecognizer
Что проверить в готовом взаимодействии на Mac?
Соберите пример в Xcode и проверьте наблюдаемое поведение, прежде чем адаптировать его к более крупной иерархии представлений:
- Щёлкните по тексту и пустой области внутри пользовательской кнопки. Каждая активация должна добавлять один круг и один раз обновлять счётчик.
- Нажмите
Command–Shift–Aв активном окне. Должно выполниться действие кнопки, добавляющее круг в следующую позицию. - Щёлкните в разных местах холста, в том числе рядом с краями. Круги должны размещаться по локальным координатам с заданным в коде отступом.
- Добавьте шесть кругов. Оба способа размещения должны прекратить добавление, а действие
Clear circlesдолжно остаться доступным. - Очистите холст. Возможность размещать фигуры должна восстановиться.
- Проверьте кнопку с помощью VoiceOver и протестируйте навигацию с клавиатуры при текущих настройках Mac. Убедитесь, что после стилизации смысл действия по-прежнему понятен.
Для пользовательской поверхности действий на Mac сначала определите смысл взаимодействия: именованной команде подходит Button, внешний вид настраивается в её метке и стиле, а размещение с учётом координат относится к жесту холста. Свяжите эти способы с одной функцией изменения состояния и добавляйте AppKit, только если его дополнительные настройки отвечают конкретному требованию.
Источники
Кнопка — документация разработчика Apple.
Жесты — документация разработчика Apple.
ButtonStyle — документация разработчика Apple.
isPressed — документация разработчика Apple.
onTapGesture(count:perform:) — документация разработчика Apple.
SpatialTapGesture — документация разработчика Apple.
init(count:coordinateSpace:) — документация разработчика Apple.
contentShape(_:eoFill:) — документация разработчика Apple.
disabled(_:) — документация разработчика Apple.
allowsHitTesting(_:) — документация разработчика Apple.
keyboardShortcut(_:modifiers:) — документация разработчика Apple.
simultaneousGesture(_:including:) — документация разработчика Apple.
highPriorityGesture(_:including:) — документация разработчика Apple.
State — документация разработчика Apple.
Замыкания — Swift.org, язык программирования Swift.
NSClickGestureRecognizer — документация разработчика Apple.
buttonMask — документация разработчика Apple.
NSGestureRecognizer — документация разработчика Apple.
NSGestureRecognizerRepresentable — документация разработчика Apple.
NSViewRepresentable — документация разработчика Apple.