Добавить объявление

Drag & Drop в SwiftUI: создаем перетаскиваемые элементы

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

Что происходит на самом деле:


Когда вы начинаете тащить элемент, SwiftUI не перемещает визуальную копию по экрану. Он упаковывает данные в специальный контейнер NSItemProvider, описывает их тип через UTType (например public.json) и только тогда система понимает, что происходит.

По сути, это конвейер: объект -> JSON -> NSItemProvider -> передача -> JSON -> объект. Пока вы тащите палец по экрану, данные еще не переехали. Они начнут передаваться только в момент, когда вы отпустите палец.

Как сделать элемент перетаскиваемым:


Создадим простую модель задачи, которую будем перетаскивать:

struct TaskItem: Identifiable, Codable {
let id = UUID()
let title: String
let priority: Int
}

Теперь сделаем сам элемент перетаскиваемым. Ключевой момент - зарегистрировать данные в NSItemProvider и указать, что мы передаем JSON:

struct DraggableTaskView: View {
let task: TaskItem

var body: some View {
HStack {
Text(task.title)
Text("\(task.priority)")
.font(.caption)
}
.padding(12)
.background(Color.blue.opacity(0.2))
.cornerRadius(8)
.onDrag {
// 1. Кодируем задачу в JSON
guard let data = try? JSONEncoder().encode(task) else {
return NSItemProvider()
}

// 2. Создаем провайдер и регистрируем данные
let provider = NSItemProvider()
provider.registerDataRepresentation(
forTypeIdentifier: UTType.json.identifier,
visibility: .all
) { completion in
completion(data, nil)
return nil
}
return provider
}
}
}

Как подготовить зону для приема:


Создадим колонку, куда можно бросать задачи. Здесь важны две вещи: визуальный отклик при наведении (чтобы пользователь понимал, что бросать можно) и асинхронная распаковка данных:

struct TaskColumn: View {
@Binding var tasks: [TaskItem]
@State private var isDropTargeted = false

var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Активные задачи")
.font(.headline)

ForEach(tasks) { task in
DraggableTaskView(task: task)
}

if tasks.isEmpty {
Text("Перетащите задачи сюда")
.foregroundColor(.gray)
.padding()
}
}
.padding()
.frame(minWidth: 250, minHeight: 300)
.background(
RoundedRectangle(cornerRadius: 16)
.fill(isDropTargeted ? Color.green.opacity(0.15) : Color.gray.opacity(0.1))
.overlay(
RoundedRectangle(cornerRadius: 16)
.stroke(isDropTargeted ? Color.green : Color.gray, lineWidth: isDropTargeted ? 2 : 1)
)
)
.onDrop(of: [.json], isTargeted: $isDropTargeted) { providers, location in
guard let provider = providers.first else { return false }

provider.loadDataRepresentation(forTypeIdentifier: UTType.json.identifier) { data, error in
guard let data = data,
let newTask = try? JSONDecoder().decode(TaskItem.self, from: data) else {
return
}

DispatchQueue.main.async {
tasks.append(newTask)
}
}
return true
}
}
}

Добавляем удаление из исходного списка:


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

// В DraggableTaskView добавляем замыкание для удаления
struct DraggableTaskView: View {
let task: TaskItem
let onDropCompleted: () -> Void // вызовется после дропа

var body: some View {
// ... тот же код
.onDrag {
// ... упаковка данных
return provider
}
}
}


А в TaskColumn при добавлении задачи можно сообщить источнику, что элемент больше не нужен - но для этого уже понадобится связь между колонками.

Что можно улучшить:


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

.onDrag {
// ... регистрация данных
return NSItemProvider(object: taskData)
} preview: {
HStack {
Image(systemName: "arrow.up.circle")
Text(task.title)
}
.padding()
.background(Color.blue)
.cornerRadius(8)
}

Вывод:


Drag & Drop в SwiftUI - это не магия, а четкий контракт: источник упаковывает данные, приемник распаковывает. NSItemProvider берет на себя передачу даже между разными приложениями, а UTType помогает системе не путать, что именно перемещается. Пользователь видит плавное перемещение, а разработчик - чистую архитектуру, где перемещаются данные, а не вьюхи.
23.03.2026 24 200