Почему обновление UI должно выполняться в main queue?

Ответ

Потому что UIKit и SwiftUI не являются потокобезопасными. Все операции с пользовательским интерфейсом должны выполняться на главном потоке (main thread). Нарушение этого правила приводит к:

  • Неожиданным крашам (часто с ошибкой Main Thread Checker).
  • Визуальным артефактам и задержкам отрисовки.
  • Непредсказуемому поведению элементов интерфейса.

Правильный паттерн:

// 1. Выполняем тяжелую работу в фоне
DispatchQueue.global(qos: .userInitiated).async {
    let processedImage = self.applyFilters(to: originalImage)

    // 2. Возвращаемся на главный поток для обновления UI
    DispatchQueue.main.async {
        self.imageView.image = processedImage
        self.activityIndicator.stopAnimating()
    }
}

SwiftUI (автоматическое управление):

Task {
    // Автоматически выполняется в фоновом потоке
    let data = await fetchData()

    // Обновление @Published/@State свойств автоматически
    // переносится на главный поток
    self.items = data
}

Исключения (но с осторожностью):

  • Создание UIImage(data:) из фонового потока допустимо, но установка в UIImageView — только на главном.
  • Некоторые вычисления layout (например, sizeThatFits) могут работать в фоне, если не обращаются к UI-иерархии.

Проверка текущего потока:

assert(Thread.isMainThread, "Этот код должен выполняться на главном потоке!")

Ответ 18+ 🔞

А, слушай, вот тебе история про главный поток, такая хуйня, что волосы дыбом встают. Представь себе, ты в своей квартире, всё по полочкам разложено, и тут вдруг какой-то левый мужик с улицы заходит и начинает мебель двигать, картины перевешивать. Что будет? Пиздец и бардак, вот что.

Так вот, UIKit и SwiftUI — это твоя квартира, а главный поток — это ты, единственный хозяин, у которого ключи. Всё, что касается интерфейса — кнопки, картинки, анимации — это священная территория, и лезть туда с грязными сапогами из другого потока — это прямой билет в краш-лэнду с ошибкой Main Thread Checker. Это как нажать на красную кнопку с надписью «Не жми, блядь».

Что будет, если накосячить:

  • Краш приложения. Не «ой, ладно», а такой пиздец, что пользователь вылетит из аппа, и доверия ебать ноль.
  • Артефакты на экране. Картинка дергается, анимация клинит, будто всё через жопу собрано.
  • Непредсказуемость. Кнопка сегодня нажимается, а завтра — хуй с горы, зависла и не отвечает.

Как делать правильно, чтобы не было мучительно больно:

// 1. Всю тяжелую, потную работу — вон, на задний двор, в фон.
DispatchQueue.global(qos: .userInitiated).async {
    let processedImage = self.applyFilters(to: originalImage) // Тут фильтры, загрузки, пиздец какой сложный код

    // 2. А как результат готов — ОБЯЗАТЕЛЬНО возвращаемся в главную хату, на основной поток!
    DispatchQueue.main.async {
        self.imageView.image = processedImage // И только тут ставим картинку
        self.activityIndicator.stopAnimating() // И крутилку выключаем
    }
}

В SwiftUI, кстати, чуть проще, там за тебя думают, хитрая жопа:

Task {
    // Эта штука сама уползет в фон, не надо руками пихать
    let data = await fetchData()

    // А тут, когда данные приползли, SwiftUI сам, как хороший дворник,
    // перенесет обновление `self.items` на главный поток. Магия, ёпта.
    self.items = data
}

Есть, конечно, исключения, но с ними как с огнем — обожрешься. Можно, например, создать UIImage из данных в фоне — это просто байты в памяти, похуй. Но вот сунуть эту картинку в UIImageView — это уже работа с интерфейсом, и делать это нужно только на главном! Иначе — в рот меня чих-пых, получишь артефакты или креш.

И на десерт, чтоб совсем спать спокойно:

assert(Thread.isMainThread, "Эй, мудила! Этот код должен выполняться на главном потоке!")

Воткни такую проверку в критичное место, и если кто-то (или ты сам) накосячит — получит по ебалу сразу на этапе отладки. Красиво и практично.