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

SwiftUI научился перетаскивать любые контейнеры в iOS 27

Раньше в SwiftUI была четкая граница. Если вы хотели реализовать перетаскивание с помощью стандартных средств ваш выбор был List с его onMove. Для LazyVStack, HStack, Grid или кастомных макетов приходилось писать решение вручную. Не было никакого готового функционала для этого.

В iOS 27 это изменилось. SwiftUI получил новый API: reorderable() и reorderContainer. Теперь любой контейнер может поддерживать перетаскивание и перестановку элементов.

Как это работает:


Новый API состоит из двух частей. Первая - reorderable(). Она ставится на динамический контент, который может участвовать в перетаскивании. Чаще всего это ForEach.

ForEach(items) { item in
ItemRow(item: item)
}
.reorderable()

Вторая - reorderContainer(for:). Она определяет родительский контейнер, внутри которого разрешена перестановка.

.reorderContainer(for: Item.self) { difference in
items.apply(difference: difference)
}

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

Простой пример:


Допустим, у вас есть плейлист в виде кастомного вертикального списка. Раньше пришлось бы реализовывать перетаскивание вручную. Теперь это всего лишь несколько строк кода:

struct PlaylistView: View {
@State private var items: [PlaylistItem] = [...]

var body: some View {
ScrollView {
LazyVStack(alignment: .leading, spacing: 8) {
ForEach(items) { item in
Text(item.title)
.padding()
.background(.thinMaterial)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
.reorderable()
}
.padding()
.reorderContainer(for: PlaylistItem.self) { difference in
items.apply(difference: difference)
}
}
}
}

reorderable() ставится на ForEach, reorderContainer - на LazyVStack. SwiftUI берет на себя всю логику жестов, а вы только обновляете данные в замыкании.

Модель данных остается за вами:


API не сохраняет новый порядок автоматически. Он дает ReorderDifference описание того, что и куда переместили. А вы уже сами решаете, как применить это к модели.

.reorderContainer(for: PlaylistItem.self) { difference in
items.apply(difference: difference)
}

Можно применить apply(difference:) к массиву. А можно обработать вручную, если логика сложнее: проверка прав на перемещение, обновление sortIndex в базе данных, синхронизация с сервером. API не навязывает конкретный способ.

Когда это особенно полезно:


Новый API открывает возможности для интерфейсов, где List не подходит. Например, доски с карточками, фотоальбомы с сеткой, избранные действия в компактных виджетах, горизонтально скроллящиеся карточки. Везде, где элементы расположены не в виде обычного списка, reorderable() дает нативное поведение без лишней работы.

Apple демонстрирует, что API работает с различными контейнерами - стеками, сетками, кастомными макетами. Код остается почти одинаковым, потому что перетаскивание не привязано к конкретному типу контейнера.

Управление доступностью перетаскивания:


У reorderContainer есть параметр isEnabled. Это удобный способ включать и выключать перестановку без условной логики на уровне жестов.

.reorderContainer(
for: PlaylistItem.self,
isEnabled: isEditing
) { difference in
items.apply(difference: difference)
}

Как только isEditing становится true, reorder активируется. Контейнер по-прежнему управляет областью взаимодействия, а состояние решает, активно ли оно.

Продвинутый сценарий - перемещение между коллекциями:


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

В этом случае reorderContainer получает дополнительный параметр - идентификатор коллекции.

.reorderContainer(for: CardValue.self, in: Card.Group.self) { difference in
game.moveCards(difference: difference)
}

А ForEach получает collectionID:

ForEach(cards, id: \.value) { card in
CardView(card: card)
}
.reorderable(collectionID: Card.Group.pile(index))

Это позволяет системе понимать, из какой группы был перемещен элемент и куда он попал.

Важный нюанс:


Reoderable - это не полноценный drag-and-drop. Он не работает с внешними данными, не поддерживает копирование вместо перемещения и не дает тонкого контроля над валидацией. Для этих задач все еще нужен общий drag-and-drop API от SwiftUI.

Вывод:


SwiftUI в iOS 27 наконец-то избавился от ограничения, которое долго раздражало разработчиков. Теперь перетаскивание работает в любом контейнере, а не только в List. Можно использовать reorderable() и reorderContainer с LazyVStack, HStack, Grid и кастомными макетами. API оставляет контроль над моделью данных за разработчиком, что важно для сложных приложений. И поддерживает перемещение между разными коллекциями.

Для пользователей это означает привычное поведение перетаскивания в любых интерфейсах. Для разработчиков - меньше ручного кода и более предсказуемый API.
24.06.2026 24 582