Для копирования синтетических фигур между двумя панелями приложения для Mac начните с типизированного перетаскивания SwiftUI. Выбирайте провайдеры элементов, если этого требует существующий интерфейс провайдера или более ранняя целевая версия macOS. Прежде чем выбирать любой из API, определите полезную нагрузку, правила принятия и владельца модели.

Определите, что означает успешное перетаскивание

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

Apple рекомендует использовать переносимые элементы для перетаскивания в Swift. В документации также описаны модификаторы на основе провайдеров, поэтому выбор зависит от требований к целевой версии и интерфейса, с которым нужно интегрироваться. Перетаскивание — Apple.

Прежде чем реализовывать представления, опишите контракт передачи данных:

Вопрос о контракте Решение для этого примера
Что пересекает границу? Версионированное значение, описывающее тип фигуры
Кто может получить представление? Только исходное приложение
Что происходит при успешном перетаскивании? В коллекцию добавляется один новый элемент с уникальным идентификатором
Кто отвечает за изменение данных? Состояние коллекции представления в месте назначения
Что происходит при отмене или отклонении? Модель не меняется
Что означает место перетаскивания? Здесь ничего; принятые элементы добавляются в конец по порядку

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

Выберите минимальный API, который выражает контракт

Ситуация С чего начать Причина
Источник и место назначения SwiftUI, macOS 13 или новее Transferable, .draggable и типизированный .dropDestination Непосредственно укажите принимаемый тип модели
Существующая интеграция с NSItemProvider .onDrag и .onDrop Сохраните явную регистрацию представления и его загрузку
Целевая версия ниже macOS 13 Проверьте доступность модификаторов на основе провайдеров для этой версии Для типизированного варианта требуется macOS 13; перегрузка на основе провайдера, рассматриваемая ниже, требует macOS 11
Существующее представление AppKit управляет взаимодействием с мышью API перетаскивания AppKit Сохраните обработку событий представлением и жизненный цикл сеанса перетаскивания

Apple описывает Transferable и исходное типизированное место назначения для перетаскивания, начиная с macOS 13. Перегрузка onDrop(of:isTargeted:perform:) на основе провайдера, принимающая массив значений UTType, доступна начиная с macOS 11. Проверяйте доступность конкретной перегрузки, а не предполагайте, что у всех API перетаскивания одна минимальная версия. Transferable, dropDestination(for:action:isTargeted:), onDrop(of:isTargeted:perform:).

Ограничить видимость собственным приложением можно при обоих подходах. Для типизированного представления задайте .visibility(.ownProcess); Apple определяет этот уровень видимости как доступный только внутри исходного приложения. Поэтому выбирать провайдер элементов только ради того, чтобы ограничить передачу приложением, не нужно. visibility(_:), ownProcess.

Реализуйте типизированное копирование

Этот законченный пример состоит из двух файлов Swift в новой целевой конфигурации приложения macOS с интерфейсом SwiftUI. Установите целевую версию macOS 13 или новее и используйте для этого базового варианта режим языка Swift 5. Замените второй файл сгенерированной точкой входа приложения; объявление @main должно быть только одно. Код проверен, но здесь не компилировался, поскольку доступная оболочка не содержит инструментария Swift.

Объявите полезную нагрузку и её тип содержимого

Добавьте объявление экспортируемого типа в список свойств информации целевой конфигурации. В редакторе списка свойств Xcode создайте UTExportedTypeDeclarations как массив с одним словарём. Задайте для этого словаря UTTypeIdentifier значение com.example.shape-transfer, UTTypeDescription значение Synthetic Shape Transfer, а UTTypeConformsTo — массив, содержащий public.data. Этот идентификатор приведён для примера; для своего приложения используйте уникальный.

Объявление Swift и идентификатор в списке свойств должны совпадать. Apple требует соответствующих записей в списке свойств для пользовательских унифицированных типов и описывает экспортируемые объявления для форматов, определённых приложением. Этот пример передаёт данные, а не файлы, поэтому расширение имени файла не добавляется. Определение типов файлов и данных для приложения.

Создайте ShapePayload.swift:

import Foundation
import CoreTransferable
import UniformTypeIdentifiers

extension UTType {
    static let shapeTransfer = UTType(
        exportedAs: "com.example.shape-transfer",
        conformingTo: .data
    )
}

enum ShapeKind: String, Codable, Sendable {
    case circle = "Circle"
    case square = "Square"
}

struct ShapePayload: Codable, Sendable, Transferable {
    let version: Int
    let kind: ShapeKind

    static var transferRepresentation: some TransferRepresentation {
        CodableRepresentation(contentType: .shapeTransfer)
            .visibility(.ownProcess)
    }
}

struct PlacedShape: Identifiable {
    let id = UUID()
    let kind: ShapeKind
}

CodableRepresentation обеспечивает преобразование полезной нагрузки в переносимые данные и обратно. Полезная нагрузка содержит значение, а не представление или ссылку на изменяемую коллекцию. Явное соответствие Sendable следует правилу Swift: хранимые свойства структуры, соответствующей этому протоколу, сами должны быть пригодны для передачи между задачами. Transferable, Sendable.

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

Примите полезную нагрузку и примените одно изменение коллекции

Создайте ShapeTransferApp.swift:

import SwiftUI

@main
struct ShapeTransferApp: App {
    var body: some Scene {
        WindowGroup {
            ShapeTransferView()
        }
    }
}

@MainActor
struct ShapeTransferView: View {
    @State private var placed: [PlacedShape] = []
    @State private var targeted = false
    @State private var message = "Drag a shape into Collection."

    var body: some View {
        HStack(alignment: .top, spacing: 24) {
            VStack(spacing: 16) {
                Text("Palette").font(.headline)
                paletteItem(.circle)
                paletteItem(.square)
            }
            .frame(width: 160)

            VStack(alignment: .leading, spacing: 12) {
                Text("Collection").font(.headline)
                Text(message)
                ForEach(placed) { item in
                    Label(item.kind.rawValue,
                          systemImage: symbol(for: item.kind))
                }
                Spacer(minLength: 20)
            }
            .padding()
            .frame(width: 300, height: 320, alignment: .topLeading)
            .background(targeted ? Color.blue.opacity(0.15)
                                 : Color.gray.opacity(0.08))
            .contentShape(Rectangle())
            .dropDestination(
                for: ShapePayload.self,
                action: { payloads, _ in
                    guard !payloads.isEmpty,
                          payloads.allSatisfy({ $0.version == 1 })
                    else {
                        message = "Drop rejected: unsupported payload."
                        return false
                    }
                    let additions = payloads.map {
                        PlacedShape(kind: $0.kind)
                    }
                    placed.append(contentsOf: additions)
                    message = "Added \(additions.count) shape(s)."
                    return true
                },
                isTargeted: { targeted = $0 }
            )
        }
        .padding(24)
    }

    private func paletteItem(_ kind: ShapeKind) -> some View {
        Label(kind.rawValue, systemImage: symbol(for: kind))
            .padding()
            .background(Color.gray.opacity(0.12))
            .draggable(ShapePayload(version: 1, kind: kind))
    }

    private func symbol(for kind: ShapeKind) -> String {
        switch kind {
        case .circle: return "circle.fill"
        case .square: return "square.fill"
        }
    }
}

Явные метки action: и isTargeted: выбирают исходную перегрузку типизированного места назначения. Apple определяет её логическое возвращаемое значение как успех или неудачу, а аргумент координат — в системе координат представления в месте назначения. Здесь координаты намеренно не используются, поскольку коллекция добавляет фигуры в конец, а не размещает их в определённой позиции. dropDestination(for:action:isTargeted:).

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

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

Используйте провайдеры элементов, если этого требует интеграция

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

  1. В замыкании .onDrag создайте NSItemProvider и зарегистрируйте представление данных под тем же пользовательским идентификатором типа, указав видимость .ownProcess.
  2. Захватите неизменяемое значение полезной нагрузки или подготовленные байты в обработчике загрузки. Закодируйте значение и вызовите обработчик завершения представления, передав данные или ошибку.
  3. Настройте .onDrop на приём пользовательского типа.
  4. В обработчике перетаскивания немедленно начните загрузку каждого принятого провайдера с помощью loadDataRepresentation(forTypeIdentifier:completionHandler:).
  5. Декодируйте полученные данные, проверьте версию и соберите результаты. Изменяйте коллекцию в главном акторе только после получения всех необходимых результатов.

Apple описывает явную регистрацию данных и видимость только внутри приложения для провайдеров. Метод загрузки работает асинхронно и возвращает объект выполнения. Совпадение идентификатора типа означает, что загрузку можно попробовать выполнить; это не гарантирует успешную загрузку и декодирование. registerDataRepresentation(forTypeIdentifier:visibility:loadHandler:), NSItemProviderRepresentationVisibility.ownProcess, loadDataRepresentation(forTypeIdentifier:completionHandler:).

Важны два ограничения по времени. Во-первых, Apple указывает, что загрузку провайдера нужно начать внутри обработчика .onDrop, хотя завершиться она может позже. Если перенести первоначальный вызов загрузки в более позднюю задачу на другом акторе, это будет нарушением документированного требования. Во-вторых, Apple предупреждает, что вызов экспорта из обработчиков представления через главный актор или главный поток может привести к зависанию. Не связывайте экспорт с изменением интерфейса; обновите состояние коллекции после завершения импорта. onDrop(of:isTargeted:perform:), onDrag(_:).

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

Сохраните управление событиями AppKit и семантику перемещения

Если существующее представление AppKit уже управляет взаимодействием с мышью, оставьте запуск сеанса в нём. Метод Apple beginDraggingSession(with:event:source:) принимает исходное событие нажатия кнопки мыши, перетаскиваемые элементы с рамками в системе координат этого представления и источник, соответствующий NSDraggingSource. Обратные вызовы источника охватывают начало сеанса, перемещение, завершение или отмену. beginDraggingSession(with:event:source:).

Граница представления данных в AppKit тоже задана явно: NSDraggingItem(pasteboardWriter:) требует объект NSPasteboardWriting. Обратный вызов провайдера SwiftUI и объект записи для буфера обмена AppKit — это разные интерфейсы. При встраивании пользовательского представления AppKit в SwiftUI оставьте за представлением AppKit ответственность за события и сеанс; общая модель должна отвечать за принятые изменения коллекции. init(pasteboardWriter:).

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

В актуальной документации Apple описаны DragConfiguration, типизированная перегрузка места назначения с сеансом и наблюдение за сеансом перетаскивания для macOS 26. Источник предлагает поддерживаемые операции, место назначения выбирает операцию, а завершение сеанса позволяет источнику отреагировать на результат. Перед использованием этого варианта проверьте доступность каждого символа в SDK. DragConfiguration, dropDestination(for:isEnabled:action:), onDragSessionUpdated(_:).

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

Проверьте контракт по наблюдаемым результатам

На Mac проверьте реализацию в следующем порядке:

  • Перетащите Circle в Collection: в месте назначения появится один круг, а элемент палитры останется на месте.
  • Повторите перетаскивание: в месте назначения появится второй, независимо идентифицированный элемент.
  • Перетащите Square за пределы места назначения и отпустите: коллекция не изменится.
  • Временно измените версию полезной нагрузки источника на 2: место назначения сообщит об отклонении и ничего не добавит; затем восстановите 1.
  • В реализации с провайдерами намеренно верните ошибку кодирования или загрузки: сообщите о сбое, не вставляя неполный пакет.
  • В реализации перемещения отмените перетаскивание и убедитесь, что исходный элемент по-прежнему принадлежит исходной коллекции.

Для задачи копирования между двумя панелями выбирайте типизированное перетаскивание SwiftUI, если нет конкретных требований совместимости или интеграции, требующих провайдеров либо AppKit. Долговечная часть решения — контракт: через границу передаётся определённое значение, место назначения проверяет его, а один владелец модели применяет принятое изменение.

Источники