Ответ
Баг: 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), некорректному отображению или пропущенным ячейкам.
Решение: Синхронизация доступа к данным.
-
Использование последовательной (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() } } } -
Использование акторов (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).
Ответ 18+ 🔞
А, слушай, вот это классика, блядь! Прямо в точку, ёпта! Сидишь такой, пишешь код, всё летает, а потом — бац! — и тебе в лог краш-репорт: NSInternalInconsistencyException. И ты сидишь, чешешь репу: «Ну как так-то? Я же в main.async обновляю!»
А оказывается, ты, гений, забыл одну маленькую деталь, блядь. Данные-то свои ты меняешь где? Правильно, в каком-то левом бэкграунде, пока твой UI, сука, уже чешется, чтобы их прочитать и отрисовать. И получается эта самая гонка, race condition, ёбаный в рот!
Представь: у тебя есть массив dataArray. Это как общая тарелка с пельменями на кухне.
- Фоновая операция (A): Ты (повар) несёшь с балкона новую, горячую кастрюлю, чтобы вывалить её в эту тарелку.
- UI-операция (B): А официант (главный поток) уже подбежал к полупустой тарелке, хватает её и несёт гостям. И несёт он, блядь, кто что получит: то ли старый холодный пельмень, то ли пустую тарелку, то ли ему в руки только что упадут два новых с кастрюли, а остальные на пол.
Вот и весь секрет, блядь. reloadData() на главном потоке — это не волшебная палочка. Она гарантирует, что отрисовка будет на главном. Но она нихуя не гарантирует, что в момент, когда таблица полезет читать dataArray для подсчёта секций и ячеек, этот массив уже будет целым и невредимым. Он может быть в процессе записи, ёпта!
Решение-то, в общем, простое, как три копейки. Надо сделать так, чтобы доступ к этой «тарелке с пельменями» был только один в один момент времени. Не можешь ты одновременно и сыпать туда, и хватать оттуда, блядь!
Вот смотри, как можно:
1. Дедовский способ, очередь.
Заводишь себе отдельную, строгую, последовательную очередь (serial queue). И говоришь всем: «Кто хочет прикоснуться к священному массиву dataArray — все в очередь, сука, строиться!». И повар, и официант.
private let dataQueue = DispatchQueue(label: "com.myapp.dataQueue") // Наша очередь вышибала
func updateData() {
// Повар встаёт в очередь на запись
dataQueue.async {
let freshPelmens = self.fetchDataFromNetwork() // Готовит на балконе
self.dataArray = freshPelmens // Вываливает в тарелку, но ТОЛЬКО когда дошла его очередь
// Теперь зовёт официанта
DispatchQueue.main.async {
self.tableView.reloadData() // Официант забирает ГОТОВУЮ тарелку
}
}
}
Теперь, когда официант (UI) в cellForRowAt полезет читать self.dataArray, он тоже должен встать в эту же очередь, но уже на чтение (dataQueue.sync), чтобы дождаться, пока повар закончит. Иначе смысла ноль, блядь.
2. Модный, молодёжный, акторы (Swift 5.5+). Это как если бы твоя «тарелка с пельменями» сама стала вышибалой. Подходишь к ней, говоришь: «Дай пельмень». А она тебе: «Стоять, блядь! Не двигаться! Щас повар допихает, тогда и получишь». И никаких очередей вручную создавать не надо.
actor DataModel { // Вот этот актор — он и есть вышибала
private var dataArray: [Item] = []
func update(with newData: [Item]) {
dataArray = newData // Повар работает. Других не подпускает.
}
func getData() -> [Item] {
return dataArray // Официант ждёт, пока повар отойдёт, и только тогда читает.
}
}
// Использование
Task {
let freshPelmens = await fetchDataFromNetwork() // Ждём на балконе
await model.update(with: freshPelmens) // Ждём, пока вышибала (актор) пропустит к тарелке
await MainActor.run { // И только ВСЁ ЗАКОНЧИВ, зовём официанта на главную сцену
tableView.reloadData()
}
}
Суть в чём, блядь? UIKit — он старый, потный и не потокобезопасный. Это как хрустальная ваза. Можно сколько угодно кричать DispatchQueue.main.async, но если ты в этот момент в другой руке этой вазой уже по столу шоркаешь — она треснет. Все манипуляции с данными, которые видны UI, должны быть либо на главном потоке, либо, если очень хочется в фоне, то прикрыты замком, очередью или актором.
А то так и будешь потом дебажить, почему у тебя в десятом запуске приложения внезапно 15 ячеек вместо 10, а в двадцатом — креш с несоответствием индексов. Пиздец, короче.