Выбирайте SwiftUI Canvas, если для перетаскивания нужны явные правила размещения, которые можно реализовать самостоятельно. Выбирайте SpriteKit, если фигуры должны толкать друг друга, отскакивать или продолжать движение под действием физики. Сначала решите, что должно происходить, когда перетаскиваемая фигура сталкивается с другой.

Это руководство посвящено одной задаче: выбору архитектуры взаимодействия для приложения Mac с синтетическими кругами или прямоугольниками, хранящимися в памяти, которые пользователь перетаскивает в рабочей области, принадлежащей приложению. Выбор зависит от поведения при столкновениях, маршрутизации ввода и того, какой компонент управляет положением каждой фигуры.

Что должно происходить, когда встречаются две фигуры?

«Реакция на столкновение» может подразумевать несколько разных требований. Подсветка перекрытия, запрет недопустимого положения и отскакивание двух объектов требуют разных механизмов. Apple явно разделяет контакты SpriteKit, которые сообщают о касании тел, и столкновения, которые рассчитывают физическую реакцию и применяют импульсы. О столкновениях и контактах

Требуемое поведение Практичная отправная точка Что остаётся под вашим контролем
Подсвечивать перекрытие, пока указатель свободно перемещает фигуру Canvas с пользовательскими геометрическими проверками Выбор объекта, обнаружение перекрытия и визуальная обратная связь
Останавливать фигуру у первого препятствия Canvas с пользовательскими ограничениями движения Проверка траектории и определение допустимого положения
Толкать соседние фигуры, позволяя им останавливаться или отскакивать SpriteKit Настройка физики и контроллер перетаскивания
Предоставить каждому элементу обычные элементы управления и жесты SwiftUI Отдельные представления SwiftUI Специфичные для приложения правила перекрытия или движения

Это архитектурные рекомендации, выведенные из задокументированных возможностей, а не пороговые показатели производительности Apple. Малое количество фигур не означает автоматически, что следует выбрать Canvas, а большое — что обязательно нужен SpriteKit. Сначала выберите поведение, затем измерьте реализацию на репрезентативном наборе элементов.

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

Что предоставляет Canvas и что нужно реализовать самостоятельно?

Canvas обеспечивает рисование в режиме immediate mode через графический контекст. Круги, контуры и символы, нарисованные в нём, не становятся отдельными интерактивными элементами. Apple также отмечает, что Canvas не обеспечивает доступность для отдельных нарисованных элементов, включая символы, переданные как представления. Поэтому жест, прикреплённый к Canvas, требует логики приложения для определения выбранного пользователем элемента. Canvas

Для редактора с явными правилами размещения храните фигуры в модели, а рисование рассматривайте как отображение этой модели. Каждый элемент может содержать идентификатор, центр, размеры и порядок рисования. В рекомендациях Swift структуры предлагаются по умолчанию для обычных данных, а классы — когда нужны общая идентичность или наследование от фреймворка. Модель элемента-значения и подкласс сцены SpriteKit решают разные задачи. Выбор между структурами и классами

Перетаскивание в Canvas может выполняться так:

  1. В начале жеста проверьте исходное положение указателя по элементам в обратном порядке рисования и выберите первый подходящий.
  2. Сохраните идентификатор выбранного элемента и его исходный центр на всё время жеста.
  3. Для каждого предполагаемого центра используйте исходный центр плюс полное смещение жеста.
  4. Примените границы рабочей области и правила столкновений, чтобы получить допустимый центр.
  5. Обновите модель; Canvas перерисует изображение на её основе.
  6. Сбросьте выбор по завершении жеста и определите очистку состояния для прерванных взаимодействий.

Важно различать полное и инкрементальное перемещение. SwiftUI определяет translation как перемещение от начала жеста. Если многократно прибавлять это полное значение к уже обновлённому центру, перемещение будет накапливаться сверх нужного. Каждый раз используйте сохранённый исходный центр. SwiftUI предоставляет через значение жеста startLocation, location и translation, а также позволяет задать пространство координат жеста. DragGesture.Value, DragGesture

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

Пример: обнаружение перекрытия и блокировка движения

Рассмотрим два условных круга радиусом 20 пунктов каждый. Круг A начинается в точке (100, 100), а неподвижный круг B находится в точке (200, 100). Пользователь задаёт центру A положение (190, 100). Расстояние между центрами составит 10 пунктов, а сумма радиусов равна 40. Эти положения перекрываются.

Для индикации перекрытия примите заданное положение и измените отрисовку. При блокировке горизонтального перетаскивания первая точка касания — (160, 100): в ней центры находятся на расстоянии 40 пунктов друг от друга. Расчёт предполагает, что B остаётся неподвижным, размеры обоих кругов не меняются, а A движется по горизонтальной траектории.

Теперь предположим, что при одном обновлении указателя запрашивается положение (260, 100). Проверка только конечной точки не обнаружит перекрытия, хотя траектория движения проходит через B. Для блокировки движения нужна проверка пересечения всей траектории или другой явный способ проверить промежуточный путь. Одной лишь отмены конечных положений с перекрытием недостаточно, чтобы реализовать поведение «остановиться у первого препятствия».

Этот пример показывает, почему важно точно определить реакцию на столкновения. Для подсветки перекрытий может хватить нескольких проверок геометрии конечной точки. Поддержка скольжения вокруг препятствий, одновременных контактов или перемещения соседних фигур значительно усложняет собственный решатель. Canvas по-прежнему может отвечать за рисование, но дополнительное поведение реализует приложение.

Когда SpriteKit становится подходящим выбором?

SpriteKit особенно удобен, когда реакция на столкновения должна возникать в ходе симуляции. Присоединение SKPhysicsBody к узлу позволяет рассчитывать гравитацию, трение и столкновения; затем симуляция обновляет положение и ориентацию узла. Для объектов поддерживаются тела-объёмы, а для границ — тела-рёбра. SKPhysicsBody

Для первого прототипа используйте два разнесённых круга и прямоугольную границу рабочей области. Назначьте кругам соответствующие тела в форме круга, отключите гравитацию для рабочей области в стиле настольной поверхности и решите, влияет ли вращение на взаимодействие. Настройте трение, упругость и демпфирование осознанно, а не полагайтесь на значения по умолчанию. Эти свойства определяют разные аспекты физического поведения. SKPhysicsBody

SwiftUI может напрямую размещать сцену с помощью SpriteView, которую Apple описывает как представление SwiftUI для отрисовки сцены SpriteKit. Согласно документации, на Mac она доступна начиная с macOS 11, а Canvas — с macOS 12. Поэтому при выборе следует учитывать целевые версии ОС, хотя главным критерием остаётся поведение взаимодействия. SpriteView, Canvas

Настройте реакцию на столкновения отдельно от уведомлений

Назначьте фигурам и границе категории, затем явно настройте две маски взаимодействия:

  • categoryBitMask определяет категории, к которым принадлежит тело.
  • collisionBitMask выбирает категории, которые могут физически воздействовать на это тело.
  • contactTestBitMask запрашивает уведомления о контактах для выбранных категорий.

Маски столкновений проверяются для каждого тела независимо. Для обычной взаимной реакции двух динамических фигур настройте оба тела так, чтобы они реагировали на категорию друг друга. Для уведомлений о контактах действует другое правило: уведомление может быть создано при совпадении категории и маски контактов в любом направлении. Включение уведомлений само по себе не настраивает физическую блокировку. collisionBitMask, contactTestBitMask

Если тела физически реагируют друг на друга, но обратный вызов не приходит, проверьте маску контактов и делегат контактов физического мира. Если обратные вызовы приходят, а фигуры проходят друг сквозь друга, проверьте маски столкновений и контроллер движения. Эти проверки помогают отделить настройку уведомлений от самой проблемы движения.

Кто управляет положением во время перетаскивания?

Для перетаскивания в SpriteKit нужен явный контроллер. Сам по себе физический движок не заставляет объекты следовать за указателем. Выберите прямое размещение или движение под управлением симуляции в соответствии с ранее определённым поведением.

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

При перетаскивании под управлением симуляции сохраняйте положение указателя как целевое и используйте силы или контроллер на основе пружины, чтобы направлять динамическое тело к нему. Тогда симуляция сможет разрешать контакты во время движения тела. Компромисс — намеренное отставание или смещение относительно указателя, когда движению мешает другой объект. SpriteKit поддерживает применение сил, а SKPhysicsJointSpring соединяет два физических тела пружиной. Это строительные блоки, требующие настройки под конкретное приложение. SKPhysicsBody, SKPhysicsJointSpring

Не задавайте положения указателя непрерывно, ожидая, что физика будет независимо управлять тем же положением. Согласно документации SpriteKit, кадр проходит через отдельные этапы: обновления приложения, действия, физика и последующие обратные вызовы. Удобное правило распределения ответственности: ввод обновляет цель перетаскивания, логика сцены применяет контроллер, а физика определяет итоговое положение тела. Обработка событий цикла кадра

У обратных вызовов контактов тоже есть документированное ограничение. Apple указывает, что они выполняются на этапе физической симуляции, когда менять физический мир нельзя, а изменение симулируемых тел приводит к неопределённому поведению. Зафиксируйте ожидающие изменения в обратном вызове и примените изменения физики при следующем обновлении, следуя описанному Apple шаблону с флагом и обновлением. SKPhysicsContactDelegate

Как ввод macOS должен проходить через границы фреймворков?

Выберите для рабочей области один компонент, отвечающий за ввод. В реализации Canvas можно прикрепить к поверхности взаимодействия SwiftUI DragGesture. Пользовательская поверхность AppKit может обрабатывать события responder; mouseDragged(with:) получает перемещения мыши с нажатой левой кнопкой. Не позволяйте жесту SwiftUI поверх этой поверхности и обработчику AppKit независимо перемещать один и тот же выбранный элемент. DragGesture, mouseDragged(with:)

Для связки AppKit и SpriteKit преобразуйте координаты на каждой границе. NSEvent.locationInWindow обычно выражается в базовой системе координат связанного окна. Преобразуйте эту точку в координаты представления-хоста, а затем — из координат представления в координаты сцены. Для последнего шага Apple предоставляет convertPoint(fromView:); перед его вызовом сцена должна быть показана. Положение узла задаётся в системе координат его родителя, поэтому для вложенных узлов может понадобиться ещё одно преобразование. locationInWindow, convertPoint(fromView:), position

Для простой интеграции со SwiftUI используйте SpriteView. Если требуется пользовательское представление AppKit, NSViewRepresentable предоставляет создание, обновление, координацию и завершение работы. Оставьте управление разметкой представленного представления за SwiftUI: Apple явно предупреждает не задавать вручную его управляемые frame или bounds. NSViewRepresentable

Храните сцену в стабильном объекте-владельце, а не создавайте новую сцену при каждом пересчёте содержимого представления SwiftUI. Для Canvas аналогично сохраняйте модель элементов. В документации по моделям SwiftUI объясняется, как управляемое состояние сохраняет экземпляр при пересоздании представления; подход с Observation требует macOS 14 или новее. Передавайте команды сцене и снимки модели осознанно, чтобы две изменяемые копии не конкурировали за управление положениями. Управление данными модели в приложении

Что протестировать, прежде чем остановиться на одном из подходов?

Проверьте сложные взаимодействия на небольшой синтетической рабочей области до добавления визуальной отделки:

  1. Захватите фигуру у края и убедитесь, что исходное смещение сохраняется.
  2. Медленно перетащите её к другому элементу, а затем повторите, проведя указателем через него одним большим перемещением.
  3. Перетащите фигуру к углу, где находятся две соседние фигуры.
  4. Отпустите фигуру, касающуюся другого элемента, и проверьте ожидаемое поведение после отпускания.
  5. Измените размер рабочей области и проверьте выбор, границы и преобразование координат.
  6. Пересоздайте окружающее содержимое SwiftUI и убедитесь, что положения элементов сохранились.
  7. Прервите перетаскивание и убедитесь, что выбор и временные настройки физики сброшены.

В SpriteKit включите SKView.showsPhysics и проверьте, соответствует ли геометрия тела предполагаемой области столкновения. Для небольших быстро движущихся тел оцените usesPreciseCollisionDetection: Apple указывает на его более высокую вычислительную стоимость и возможность обнаруживать столкновения, которые могут пропустить обычные проверки на основе кадров. Он не должен заменять продуманный контроллер перетаскивания. SKView, usesPreciseCollisionDetection

Выбирайте Canvas, если правила размещения остаются понятными, ограниченными и их проще выразить напрямую. Выбирайте SpriteKit, если физическое взаимодействие между фигурами — часть задачи. В любом случае для надёжного перетаскивания на компьютере нужны один выбранный элемент, явные преобразования координат и один компонент, отвечающий за каждое обновление положения.

Источники