Опиши пример типичного бага, связанного с многопоточностью и обновлением UI в 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).

Ответ 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, а в двадцатом — креш с несоответствием индексов. Пиздец, короче.