Опиши пример типичного бага, связанного с многопоточностью и обновлением UI в iOS.

«Опиши пример типичного бага, связанного с многопоточностью и обновлением UI в iOS.» — вопрос из категории Other, который задают на 10% собеседований IOS Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Баг: Race condition при обновлении UITableView/UICollectionView из фонового потока.

Сценарий: Данные изменяются асинхронно (например, при загрузке из сети), а затем интерфейс обновляется вызовом reloadData() на главном потоке. Если изменения данных и обновление UI не синхронизированы, возникает состояние гонки.

Проблемный код:

// ФОНТОВЫЙ ПОТОК: данные изменяются
DispatchQueue.global().async {
    let newData = self.fetchDataFromNetwork()
    self.dataArray = newData // ОПЕРАЦИЯ ЗАПИСИ (A)

    // ГЛАВНЫЙ ПОТОК: UI обновляется
    DispatchQueue.main.async {
        self.tableView.reloadData() // ОПЕРАЦИЯ ЧТЕНИЯ (B) dataArray
    }
}

Проблема: Нет гарантии, что операция записи (A) завершится до операции чтения (B). UI может прочитать частично обновленные или неконсистентные данные, что приведет к крешам (NSInternalInconsistencyException), некорректному отображению или пропущенным ячейкам.

Решение: Синхронизация доступа к данным.

  1. Использование последовательной (serial) очереди:

    private let dataQueue = DispatchQueue(label: "com.app.dataQueue")
    
    func updateData() {
        dataQueue.async {
            let newData = self.fetchDataFromNetwork()
            self.dataArray = newData
    
            DispatchQueue.main.async {
                self.tableView.reloadData()
            }
        }
    }
  2. Использование акторов (Actor) в Swift 5.5+:

    actor DataModel {
        private var dataArray: [Item] = []
    
        func update(with newData: [Item]) {
            dataArray = newData
        }
    
        func getData() -> [Item] {
            return dataArray
        }
    }
    
    // Использование
    Task {
        let newData = await fetchDataFromNetwork()
        await model.update(with: newData)
        await MainActor.run {
            tableView.reloadData()
        }
    }

Почему это важно: UI-компоненты UIKit не потокобезопасны. Все операции с данными, которые отображаются или изменяют UI, должны выполняться на главном потоке (MainActor).