← Назад к вопросам

Что такое Diffable Data Source?

2.0 Middle🔥 202 комментариев
#UIKit и верстка

Комментарии (2)

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Что такое Diffable Data Source?

Diffable Data Source — это современный API, представленный в UIKit (iOS 13+ и tvOS 13+) и SwiftUI, который революционизирует управление данными в таблицах (UITableView) и коллекциях (UICollectionView). Его основная цель — автоматическое и безопасное вычисление различий (diff) между текущим и новым состоянием данных, что позволяет выполнять анимированные обновления интерфейса без ручных манипуляций с секциями и элементами.

Ключевые принципы и преимущества

Идентификация через идентификаторы

В отличие от традиционного DataSource, который опирается на индексы (IndexPath), Diffable Data Source использует уникальные идентификаторы для каждой секции и элемента. Это позволяет системе точно отслеживать изменения, даже когда данные перемещаются или изменяют порядок.

  • Секции: идентифицируются через NSDiffableDataSourceSnapshot.SectionIdentifier (например, String или кастомный тип).
  • Элементы: идентифицируются через NSDiffableDataSourceSnapshot.ItemIdentifier (например, модель данных или UUID).

Снимки (Snapshots) как состояние

Вместо прямого изменения DataSource вы работаете с снимками (NSDiffableDataSourceSnapshot). Вы создаете новый снимок, описывающий желаемое состояние данных (секции и элементы в определённом порядке), и применяете его к DataSource. Система автоматически вычисляет diff между предыдущим и новым снимками и применяет необходимые изменения к интерфейсу с анимацией.

Пример использования с UITableView

import UIKit

// 1. Определяем идентификаторы. Они должны быть Hashable.
struct Task: Hashable {
    let id: UUID
    let title: String
    let isCompleted: Bool
}

class TasksViewController: UIViewController {
    @IBOutlet weak var tableView: UITableView!
    
    // 2. Создаем Diffable Data Source.
    private var dataSource: UITableViewDiffableDataSource<String, Task>!
    
    private var tasks: [Task] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        configureDataSource()
        loadInitialData()
    }
    
    private func configureDataSource() {
        // 3. Инициализируем Data Source, связывая его с таблицей и определяя ячейку.
        dataSource = UITableViewDiffableDataSource<String, Task>(tableView: tableView) { tableView, indexPath, task in
            let cell = tableView.dequeueReusableCell(withIdentifier: "TaskCell", for: indexPath)
            cell.textLabel?.text = task.title
            cell.accessoryType = task.isCompleted ? .checkmark : .none
            return cell
        }
    }
    
    private func loadInitialData() {
        tasks = [
            Task(id: UUID(), title: "Изучить Diffable Data Source", isCompleted: true),
            Task(id: UUID(), title: "Реализовать пример", isCompleted: false)
        ]
        
        // 4. Создаем и применяем начальный снимок.
        var snapshot = NSDiffableDataSourceSnapshot<String, Task>()
        snapshot.appendSections(["Основные"]) // Добавляем секцию с идентификатором "Основные"
        snapshot.appendItems(tasks, toSection: "Основные") // Добавляем задачи в секцию
        dataSource.apply(snapshot, animatingDifferences: true) // Применяем с анимацией
    }
    
    func addNewTask(title: String) {
        let newTask = Task(id: UUID(), title: title, isCompleted: false)
        tasks.append(newTask)
        
        // 5. Для обновления создаем новый снимок, отражающий текущее состояние.
        var snapshot = dataSource.snapshot() // Начинаем с текущего снимка
        snapshot.appendItems([newTask], toSection: "Основные")
        dataSource.apply(snapshot, animatingDifferences: true)
    }
    
    func toggleTaskCompletion(at index: Int) {
        tasks[index].isCompleted.toggle()
        
        // 6. Обновляем существующий элемент. Diffable Data Source автоматически обнаружит изменение.
        var snapshot = dataSource.snapshot()
        // Элемент уже присутствует в снимке, но его состояние (isCompleted) изменилось.
        // Мы можем просто "перезагрузить" этот элемент в снимке.
        snapshot.reloadItems([tasks[index]])
        dataSource.apply(snapshot, animatingDifferences: true)
    }
}

Почему это лучше традиционного DataSource?

  • Безопасность: Индексы (IndexPath) могут стать невалидными после изменений данных, что приводит к ошибкам (например, index out of range). Diffable Data Source устраняет эту проблему, работая через уникальные идентификаторы.
  • Производительность: Алгоритм diffing эффективно вычисляет минимальный набор изменений, что оптимизирует производительность, особенно для больших наборов данных.
  • Автоматическая анимация: Все изменения (добавление, удаление, перемещение) автоматически анимируются. В традиционном подходе вам приходилось самостоятельно вызывать методы типа insertRows, deleteRows, moveRow, что было сложно и подвержено ошибкам.
  • Упрощение кода: Вам не нужно управлять состоянием таблицы/коллекции. Вы просто описываете желаемое состояние данных через снимок.

Важные ограничения и особенности

  • Идентификаторы должны быть Hashable: Это требование для корректного сравнения.
  • Избегайте изменений идентификаторов: Если вы изменяете свойства объекта, которые влияют на его hashValue или ==, это может привести к непредсказуемому поведению. Для таких случаев используйте reloadItems.
  • Порядок имеет значение: Порядок секций и элементов в снимке определяет их порядок в интерфейсе.

В SwiftUI

В SwiftUI концепция Diffable Data Source воплощена в самих структурах данных. Например, List и ForEach автоматически реагируют на изменения в массивах данных, если элементы соответствуют Identifiable. SwiftUI внутренне использует механизмы diffing для анимированных обновлений.

Diffable Data Source стал стандартом де-факто для работы с коллекциями и таблицами в iOS, значительно повышая надежность, производительность и удобство разработки динамических интерфейсов.