Списки и динамические данные с 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.

Проверьте себя
1. Зачем модели в ForEach нужен протокол Identifiable?
AЧтобы ускорить сеть
BЧтобы SwiftUI мог отличать строки и корректно обновлять список
CЧтобы сохранять данные на диск
DЭто требование компилятора для всех структур
2. Почему индекс массива — плохой выбор для идентификации строк?
AИндексы слишком длинные
BПри удалении элементов индексы сдвигаются и SwiftUI путает строки
CИндексы нельзя сравнивать
DЭто запрещено синтаксисом