Когда можно обновлять UI не в главном потоке в iOS?

Ответ

Основное правило: UI-компоненты UIKit/AppKit можно обновлять только в главном потоке, так как они не потокобезопасны.

Исключение: Метод drawHierarchy(in:afterScreenUpdates:) можно вызывать в фоновом потоке для рендеринга в изображение, например при создании скриншотов.

Пример легального использования в фоновом потоке:

DispatchQueue.global(qos: .background).async {
    let renderer = UIGraphicsImageRenderer(size: view.bounds.size)
    let screenshot = renderer.image { context in
        // Это безопасно в фоновом потоке
        view.drawHierarchy(in: view.bounds, afterScreenUpdates: false)
    }

    // Далее работаем с изображением (сохранение, обработка)
    UIImageWriteToSavedPhotosAlbum(screenshot, nil, nil, nil)
}

Стандартный подход для UI-обновлений:

// Всегда используйте DispatchQueue.main для UI изменений
DispatchQueue.main.async {
    self.label.text = "Новый текст"
    self.button.isEnabled = false
    self.view.backgroundColor = .systemBlue
}

Важно: SwiftUI требует обновления состояния также в главном потоке через @MainActor или DispatchQueue.main.

Ответ 18+ 🔞

Ага, слушай, вот сидишь ты такой, пишешь свой код, и тут — бац! — нужно UI обновить. И ты такой: "Да похуй, сделаю в бэкграунде". А потом — охуенно! — приложение крешится, и ты сидишь, чешешь репу: "Ну что за хуйня?"

Так вот, запомни раз и навсегда, как отче наш: все эти ваши UIKit/AppKit штуки — они, блядь, не потокобезопасные. Совсем. Вообще. Нихуя. Обновлять их можно только в главном потоке. Это как закон природы, типа гравитации. Попробуешь нарушить — получишь ебучую ошибку и креш в самом интересном месте.

Но! Как и в любом правиле, тут есть одна хитрая жопа, одно исключение. Метод drawHierarchy(in:afterScreenUpdates:). Его, сука, можно вызывать в фоновом потоке, но только для одной цели — чтобы нарисовать вьюху в картинку. Например, когда тебе скриншот нахуйрить нужно. Вот так:

DispatchQueue.global(qos: .background).async {
    let renderer = UIGraphicsImageRenderer(size: view.bounds.size)
    let screenshot = renderer.image { context in
        // Это безопасно в фоновом потоке
        view.drawHierarchy(in: view.bounds, afterScreenUpdates: false)
    }

    // Далее работаем с изображением (сохранение, обработка)
    UIImageWriteToSavedPhotosAlbum(screenshot, nil, nil, nil)
}

Видишь? Всё чётко. Рендерим в фоне, получаем UIImage, и потом уже с ним делаем что хотим. А вот лезть из фонового потока и пытаться поменять text у лейбла или backgroundColor — это прямой билет в ад, чувак. Там тебя ждёт неопределённое поведение, гонки данных и прочий пиздец.

Поэтому для обычных UI-апдейтов — только так, блядь:

// Всегда используйте DispatchQueue.main для UI изменений
DispatchQueue.main.async {
    self.label.text = "Новый текст"
    self.button.isEnabled = false
    self.view.backgroundColor = .systemBlue
}

Вот это — святое. Запомнил? И да, это касается не только старого доброго UIKit. Твой модный SwiftUI тоже, ёпта, требует, чтобы состояние обновлялось в главном потоке. Через @MainActor или тот же DispatchQueue.main. Не выёбывайся, делай как все умные люди.

Короче, правило простое: думаешь потрогать UI — сначала спроси себя: "А я в главном потоке?" Если нет — оберни в DispatchQueue.main.async и живи спокойно. Иначе будешь потом ночами дебажить, почему всё ебётся.