Используйте кнопку 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:)

Так для видимых поверхностей задаются чёткие границы взаимодействия. Если при редизайне вы вынесете рамку за пределы метки кнопки, проверьте результат ещё раз: в этом примере размеры и форма содержимого связаны внутри метки. Продуманно задавайте область рисования, компоновки и взаимодействия вместо того, чтобы исправлять не реагирующий нажатиям отступ добавлением ещё одного обработчика.

Если поверхность перестала реагировать, проверьте следующее по порядку:

  1. Проверьте геометрию. Щёлкните по тексту метки, графике и пустой области внутри неё. Убедитесь, что форма содержимого описывает нужные границы.
  2. Проверьте отключённые родительские представления. Отключённый контейнер переопределяет состояние включённого дочернего элемента, поэтому локальный .disabled(false) может не восстановить взаимодействие. disabled(_:)
  3. Проверьте декоративные перекрытия. Если представление служит только для оформления, его можно исключить из проверки нажатий с помощью .allowsHitTesting(false). Применяйте это к декору, а не к интерактивной поверхности. allowsHitTesting(_:)
  4. Проверьте конкурирующие обработчики. Прежде чем менять приоритет жестов, определите, какое представление отвечает за действие.

Как избежать конкурирующих обработчиков и ошибок при распределении ответственности?

Используйте один обработчик для каждого предусмотренного способа активации. В примере пользовательская кнопка отвечает за команду, а холст — за размещение по координатам. Ни один из них не находится внутри родительского представления с дополнительным жестом размещения. Благодаря такой структуре легко понять, к чему приведёт один щелчок.

Не добавляйте второй обработчик касания к кнопке только ради вызова той же функции изменения состояния. Кроме того, не добавляйте жест к родительскому представлению с несколькими элементами управления, не решив, должны ли взаимодействия родителя и дочерних элементов выполняться одновременно. В примере 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, только если его дополнительные настройки отвечают конкретному требованию.

Источники