Раньше в
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.