Списки и динамические данные с ForEach
Большинство экранов — это списки: задачи, сообщения, товары. SwiftUI строит их из List и ForEach на основе ваших данных.
Суть урока: ForEach превращает массив данных в набор вью. Чтобы SwiftUI отличал строки друг от друга, каждый элемент должен быть уникально идентифицируемым — отсюда протокол Identifiable.
List создаёт прокручиваемый список с нативным оформлением, а ForEach генерирует строки из коллекции. Чтобы SwiftUI корректно отслеживал строки при изменениях, ему нужен способ их различать. Самый чистый способ — сделать модель Identifiable:
struct Task: Identifiable {
let id = UUID() // уникальный идентификатор
var title: String
var done = false
}
struct TaskList: View {
@State private var tasks = [
Task(title: "Учить Swift"),
Task(title: "Сделать приложение")
]
var body: some View {
List {
ForEach(tasks) { task in
Text(task.title)
}
}
}
}Поскольку Task соответствует Identifiable (имеет свойство id), ForEach(tasks) работает без дополнительных параметров. Если тип не идентифицируем, придётся явно указать ключ: ForEach(items, id: \.self). Идентичность критична — по ней SwiftUI понимает, какая строка добавилась, удалилась или переместилась, и анимирует изменения.
Списки умеют редактироваться. Добавим удаление свайпом:
List {
ForEach(tasks) { task in
Text(task.title)
}
.onDelete { offsets in
tasks.remove(atOffsets: offsets)
}
}Массив данных ForEach по id List на экране
[Task(id:A), --> строка для A --> +----------------+
Task(id:B), строка для B | Учить Swift |
Task(id:C)] строка для C | Сделать прилож |
+----------------+
добавили Task(id:D) -> SwiftUI вставит ОДНУ новую строкуПопробуй сам ▶ — запусти код прямо в браузере (Pyodide). Здесь нет Swift, но логика та же, что под капотом мобильного кода:
# Identifiable + рендеринг списка: каждая строка по уникальному id.
import itertools
_counter = itertools.count(1)
def make_task(title):
return {'id': next(_counter), 'title': title, 'done': False}
tasks = [make_task('Учить Swift'), make_task('Сделать приложение')]
def render_list(items):
for t in items:
mark = '[x]' if t['done'] else '[ ]'
print(f"{mark} #{t['id']} {t['title']}")
render_list(tasks)
print('--- добавили строку, удалили первую ---')
tasks.append(make_task('Опубликовать'))
tasks = [t for t in tasks if t['id'] != 1] # как onDelete
render_list(tasks)Как работает под капотом
ForEach не просто рисует элементы — он сопоставляет старый и новый наборы по идентификаторам. Если id строки сохранился, вью переиспользуется; если появился новый id — вставляется строка; исчез — удаляется. Эта диффинг-логика по идентичности и даёт плавные анимации вставки/удаления «из коробки». Вот почему стабильный, уникальный id так важен: меняющиеся идентификаторы ломают анимации и состояние строк.
Частые ошибки
- Использовать индекс массива как id. При удалении индексы сдвигаются, и SwiftUI путает строки.
- Нестабильный id. Если id вычисляется заново при каждой отрисовке, анимации и состояние ломаются.
- Забыть Identifiable. Тогда ForEach потребует явный параметр id.
Best practices
- Делайте модели Identifiable со стабильным id (например, UUID).
- Используйте .onDelete и .onMove для редактируемых списков.
- Не применяйте id: \.self для изменяемых данных — только для неизменных уникальных значений.
Итоги. List и ForEach строят динамические списки из данных, а протокол Identifiable даёт SwiftUI стабильную идентичность строк для корректного обновления и анимаций. Это основа большинства экранов реальных приложений.
Шире контекста
Идентичность — тихий герой динамических списков, и недооценивать её опасно. Стабильный уникальный id позволяет SwiftUI понимать не просто «список изменился», а конкретно «эта строка добавилась, эта удалилась, а эта переехала», и анимировать ровно это. Когда id нестабилен или совпадает с индексом массива, вы получаете дёргающиеся анимации, потерю состояния строк (например, сброшенное поле ввода внутри ячейки) и трудноуловимые баги. Поэтому UUID или серверный идентификатор почти всегда лучше индекса. List в SwiftUI даёт из коробки богатую функциональность: разделители, секции, свайп-действия, перетаскивание для перестановки, режим редактирования — и всё это управляется данными, а не ручными командами. По мере усложнения приложения вы будете комбинировать List с поиском, фильтрацией и асинхронной подгрузкой, но в основе всегда будет лежать связка надёжной модели Identifiable и декларативного ForEach.