Перед началом отметьте, что это не имеет никакого отношения к фоновой обработке. Нет никакого "расчета", который можно было бы использовать.
(Примечание "одна секунда" - просто простой пример для ясности. См. конец этого сообщения для более полного примера.)
Вы можете попробовать следующее. Кажется, что это не работает.
Разумный. Но наше недавнее тестирование в реальной жизни показывает, что во многих случаях это НЕ работает.
(т.е. Apple UIKit теперь достаточно сложна, чтобы смазать работу UIKit за пределами этого "трюка".)
Мысль: есть ли способ, в UIKit, получить обратный вызов, когда он, в основном, нарисовал все просмотренные вами взгляды? Есть ли другое решение?
Кажется, что одно из решений... поместит subviews в контроллеры, поэтому вы получаете обратный вызов "didAppear" и отслеживаете их. Это кажется инфантильным, но, может быть, это единственный шаблон? Будет ли это действительно работать? (Только одна проблема: я не вижу никакой гарантии, что didAppear гарантирует, что все подзаголовки будут нарисованы.)
• часто возникает утомительная пауза для пользователя, поскольку появляется "новый экран". (.1 до .5 или хуже).
• Если вы делаете то, о чем я прошу, он всегда будет сглаживать экран, по одному фрагменту за раз, с минимально возможным временем для каждого фрагмента.
Ответ 2
TL;DR
Включение ожидающих изменений пользовательского интерфейса на сервере рендеринга с помощью CATransaction.flush()
или разделить работу на несколько фреймов с помощью CADisplayLink
(пример кода ниже).
Резюме
Есть ли способ, в UIKit, получить обратный вызов, когда он нарисовал все просмотренные вами представления?
Нет
iOS действует как изменение рендеринга игры (независимо от того, сколько вы делаете) не более одного раза за кадр. Единственный способ гарантировать прохождение кода после того, как ваши изменения были отображены на экране, - это ждать следующего кадра.
Есть ли другое решение?
Да, iOS может обрабатывать изменения только один раз за кадр, но ваше приложение не является тем, что делает этот рендеринг. Процесс оконного сервера.
Ваше приложение делает свой макет и рендеринг, а затем фиксирует его изменения на свой layerTree на сервере рендеринга. Он будет делать это автоматически в конце runloop, или вы можете принудительно отправить незавершенные транзакции на сервер визуализации CATransaction.flush()
.
Однако блокирование основного потока в целом плохо (не только потому, что он блокирует обновления пользовательского интерфейса). Поэтому, если вы можете этого избежать,
Возможные решения
Это интересующая вас часть.
1: сделайте как можно больше фоновой очереди так, как сможете, и повысите производительность.
Серьезно iPhone 7 является третьим самым мощным компьютером (не телефоном) в моем доме, только избитым моим игровым ПК и Macbook Pro. Это быстрее, чем каждый другой компьютер в моем доме. Для отображения пользовательского интерфейса приложений не требуется пауза в 3 секунды.
2: скрытые ожидания CAT-транзакций
EDIT: Как указано rob mayoff, вы можете заставить CoreAnimation отправить ожидающие изменения в сервер рендеринга, вызвав CATransaction.flush()
addItems1()
CATransaction.flush()
addItems2()
CATransaction.flush()
addItems3()
Это фактически не приведет к изменениям, но отправляет ожидающие обновления пользовательского интерфейса на оконный сервер, гарантируя, что они включены в следующее обновление экрана.
Это будет работать, но с этим предупреждением в документации для Apple.
Однако вам следует попытаться избежать явного вызова вызова flush. Предоставляя флеш выполнять во время runloop...... и транзакции и анимации, которые работают от транзакции к транзакции, будут продолжать функционировать.
Однако заголовочный файл CATransaction
включает эту цитату, которая, по-видимому, подразумевает, что даже если им это не нравится, это официально поддерживается.
В некоторых случаях (т.е. никакой цикл цикла или цикл цикла не блокируется) может потребоваться использование явных транзакций для получения своевременных обновлений дерева рендеринга.
Документация для Apple - "Лучшая документация для + [CATransaction flush ]" .
3: dispatch_after()
Просто задержите код до следующей runloop. dispatch_async(main_queue)
не будет работать, но вы можете использовать dispatch_after()
без задержки.
addItems1()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.0) {
addItems2()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.0) {
addItems3()
}
}
Вы упомянули в своем ответе, что это больше не работает для вас. Тем не менее, он отлично работает в тестовой Swift Playground и примере приложения iOS, которое я включил с этим ответом.
4: Используйте CADisplayLink
CADisplayLink вызывается один раз для каждого кадра и позволяет гарантировать выполнение только одной операции для каждого кадра, гарантируя, что экран сможет обновляться между операциями.
DisplayQueue.sharedInstance.addItem {
addItems1()
}
DisplayQueue.sharedInstance.addItem {
addItems2()
}
DisplayQueue.sharedInstance.addItem {
addItems3()
}
Требуется, чтобы этот вспомогательный класс работал (или похожим).
// A queue of item that you want to run one per frame (to allow the display to update in between)
class DisplayQueue {
static let sharedInstance = DisplayQueue()
init() {
displayLink = CADisplayLink(target: self, selector: #selector(displayLinkTick))
displayLink.add(to: RunLoop.current, forMode: RunLoopMode.commonModes)
}
private var displayLink:CADisplayLink!
@objc func displayLinkTick(){
if let _ = itemQueue.first {
itemQueue.remove(at: 0)() // Remove it from the queue and run it
// Stop the display link if it not needed
displayLink.isPaused = (itemQueue.count == 0)
}
}
private var itemQueue:[()->()] = []
func addItem(block:@escaping ()->()) {
displayLink.isPaused = false // It needed again
itemQueue.append(block) // Add the closure to the queue
}
}
5: вызовите runloop напрямую.
Мне это не нравится из-за возможности бесконечного цикла. Но, признаюсь, это маловероятно. Я также не уверен, что это официально поддерживается, или инженер Apple собирается прочитать этот код и выглядеть в ужасе.
// Runloop (seems to work ok, might lead to infitie recursion if used too frequently in the codebase)
addItems1()
RunLoop.current.run(mode: .defaultRunLoopMode, before: Date())
addItems2()
RunLoop.current.run(mode: .defaultRunLoopMode, before: Date())
addItems3()
Это должно работать, если только (хотя и не реагирует на события runloop) вы делаете что-то еще, чтобы блокировать вызов runloop от завершения, поскольку CATransaction отправляется на оконный сервер в конце runloop.
Пример кода
Демонстрационный проект Xcode и игровая площадка Xcode (Xcode 8.2, Swift 3)
Какой вариант использовать?
Мне нравятся решения DispatchQueue.main.asyncAfter(deadline: .now() + 0.0)
и CADisplayLink
лучшие. Тем не менее, DispatchQueue.main.asyncAfter
не гарантирует, что он будет работать на следующей отметке runloop, чтобы вы не захотели доверять ей?
CATransaction.flush()
заставит вас изменить настройки пользовательского интерфейса на сервер рендеринга, и это использование, по-видимому, соответствует комментариям Apple к классу, но содержит некоторые предупреждения.
В некоторых случаях (т.е. никакой цикл цикла или цикл цикла не блокируется) может потребоваться использование явных транзакций для получения своевременных обновлений дерева рендеринга.
Подробное объяснение
Остальная часть этого ответа - это предыстория того, что происходит внутри UIKit, и объясняет, почему исходные ответы на попытки использовать view.setNeedsDisplay()
и view.layoutIfNeeded()
ничего не сделали.
Обзор макета и рендеринга UIKit
CADisplayLink полностью не связан с UIKit и runloop.
Не совсем. iOS UI - это графический процессор, созданный как 3D-игра. И пытается сделать как можно меньше. Поэтому многие вещи, такие как макет и рендеринг, не происходят, когда что-то меняется, но когда это необходимо. Вот почему мы называем setNeedsLayout не макетными представлениями. Каждый кадр может меняться несколько раз. Тем не менее, iOS будет пытаться вызывать только layoutSubviews
один раз за кадр, а не 10 раз setNeedsLayout
может быть вызван.
Тем не менее, очень много происходит на процессоре (макет, -drawRect:
и т.д.), так как все это сочетается.
Обратите внимание, что это все упрощено и пропускает множество вещей, таких как CALayer, фактически являющийся реальным объектом просмотра, который показывает на экране не UIView и т.д.
Каждый UIView можно рассматривать как растровое изображение, текстуру изображения/графического процессора. Когда экран отображается, графический процессор объединяет иерархию представления в полученный в результате фрейм, который мы видим. Он формирует представления, превращая текстуры subviews поверх предыдущих просмотров в готовый рендер, который мы видим на экране (аналогично игре).
Это то, что позволило iOS иметь такой плавный и легко анимированный интерфейс. Чтобы анимировать представление по экрану, ему не нужно ничего переписывать. На следующем кадре, который просматривает текстуру, просто составлен в несколько другом месте на экране, чем раньше. Ни он, ни мнение, что он был в избытке, чтобы их содержимое было перезаписано.
В прошлом общий совет по производительности обычно заключался в том, чтобы сократить количество просмотров в иерархии представлений, полностью отображая ячейки представления в drawRect:
. Этот совет должен был сделать шаг компостирования GPU быстрее на ранних устройствах iOS. Тем не менее, графический процессор настолько быстро работает на современных устройствах iOS, что сейчас больше не беспокоит.
LayoutSubviews и DrawRect
-setNeedsLayout
аннулирует текущий макет представления и отмечает его как необходимый макет.
-layoutIfNeeded
передаст представление, если у него нет допустимого макета
-setNeedsDisplay
будет отмечать виды, которые необходимо перерисовать. Ранее мы говорили, что каждый вид визуализируется в текстуру/изображение представления, которое можно перемещать и манипулировать с помощью графического процессора без необходимости перерисовывать. Это вызовет его перерисовку. Чертеж выполняется путем вызова -drawRect:
на ЦПУ и, следовательно, медленнее, чем возможность полагаться на графический процессор, который может выполнять большинство кадров.
И важно отметить, что эти методы не делают. Методы компоновки ничего не делают визуально. Хотя если для представлений contentMode
установлено значение redraw
, изменение кадра просмотров может привести к аннулированию визуализации представлений (trigger -setNeedsDisplay
).
Вы можете попробовать следующее в течение всего дня. Кажется, это не работает:
view.addItemsA()
view.setNeedsDisplay()
view.layoutIfNeeded()
view.addItemsB()
view.setNeedsDisplay()
view.layoutIfNeeded()
view.addItemsC()
view.setNeedsDisplay()
view.layoutIfNeeded()
Из того, что мы узнали, ответ должен быть очевидным, почему это не работает сейчас.
view.layoutIfNeeded()
ничего не делает, кроме пересчета кадров его подзонов.
view.setNeedsDisplay()
просто помечает представление так, как требуется перерисовка в следующий раз. UIKit просматривает иерархию представлений, обновляя текстуры просмотра для отправки на GPU. Однако, это не влияет на подсмотры, которые вы пытались добавить.
В вашем примере view.addItemsA()
добавлено 100 подменю. Это отдельные несвязанные слои/текстуры на GPU, пока GPU не объединит их в следующий фреймбуфер. Единственное исключение - если у CALayer shouldRasterize
установлено значение true. В этом случае он создает отдельную текстуру для представления, а ее подматривает и визуализирует (думает на графическом процессоре) представление, и он подглядывает в одну текстуру, эффективно кэшируя композицию, которую он должен будет выполнять каждый кадр. Это имеет преимущество в производительности, когда не нужно составлять все его подпрограммы в каждом кадре. Тем не менее, если вид или его просмотры часто меняются (например, во время анимации), это будет штраф за производительность, так как это приведет к аннулированию кешированной текстуры, часто требующей ее перерисовывания (аналогично частому вызову -setNeedsDisplay
).
Теперь любой инженер игры просто сделает это...
view.addItemsA()
RunLoop.current.run(mode: .defaultRunLoopMode, before: Date())
view.addItemsB()
RunLoop.current.run(mode: .defaultRunLoopMode, before: Date())
view.addItemsC()
Теперь, действительно, это работает.
Но почему это работает?
Теперь -setNeedsLayout
и -setNeedsDisplay
не запускают ретрансляцию или перерисовку, а вместо этого просто отмечают вид как необходимый. По мере того, как UIKit готовится к рендерингу следующего кадра, он вызывает представления с недопустимыми текстурами или макетами для перерисовки или ретрансляции. После того, как все будет готово, он отправляет команду GPU в композицию и отображает новый кадр.
Итак, основной цикл запуска в UIKit, вероятно, выглядит примерно так.
-(void)runloop
{
//... do touch handling and other events, etc...
self.windows.recursivelyCheckLayout() // effectively call layoutIfNeeded on everything
self.windows.recursivelyDisplay() // call -drawRect: on things that need it
GPU.recompositeFrame() // render all the layers into the frame buffer for this frame and displays it on screen
}
Итак, вернемся к исходному коду.
view.addItemsA() // Takes 1 second
view.addItemsB() // Takes 1 second
view.addItemsC() // Takes 1 second
Итак, почему все 3 изменения появляются сразу после 3 секунд вместо одного за раз на 1 секунду?
Хорошо, если этот бит кода запущен в результате нажатия кнопки или аналогичного, он выполняет синхронную блокировку основного потока (поток UIKit требует внесения изменений пользовательского интерфейса) и поэтому блокирует цикл выполнения в строке 1, четная обрабатывающая часть. Фактически, вы делаете, чтобы первая строка метода runloop возвращалась за 3 секунды.
Однако мы определили, что макет не будет обновляться до строки 3, отдельные представления не будут отображаться до тех пор, пока строка 4 и никакие изменения не будут отображаться на экране до последней строки метода runloop, строка 5.
Причина, по которой вручную запускается runloop, заключается в том, что вы в основном вставляете вызов методу runloop()
. Ваш метод работает в результате вызова из функции runloop
-runloop()
- events, touch handling, etc...
- addLotsOfViewsPressed():
-addItems1() // blocks for 1 second
-runloop()
| - events and touch handling
| - layout invalid views
| - redraw invalid views
| - tell GPU to composite and display a new frame
-addItem2() // blocks for 1 second
-runloop()
| - events // hopefully nothing massive like addLotsOfViewsPressed()
| - layout
| - drawing
| - GPU render new frame
-addItems3() // blocks for 1 second
- relayout invalid views
- redraw invalid views
- GPU render new frame
Это будет работать, если оно не используется очень часто, потому что оно использует рекурсию. Если он часто используется, каждый вызов -runloop
может вызвать другой, приводящий к рекурсии бегства.
КОНЕЦ
Ниже приведено только уточнение.
Дополнительная информация о том, что здесь происходит
CADisplayLink и NSRunLoop
Если я не ошибаюсь, KH кажется, что в принципе вы считаете, что "цикл запуска" (то есть: этот: RunLoop.current) - это CADisplayLink.
Runloop и CADisplayLink - это не одно и то же. Но CADisplayLink подключается к runloop для работы.
Я немного оговорился раньше (в чате), когда я сказал, что NSRunLoop называет CADisplayLink каждым тиком, это не так. Насколько я понимаю, NSRunLoop - это в основном цикл while (1), который заключается в том, чтобы сохранить поток в действии, обрабатывать события и т.д. Чтобы избежать скольжения, я собираюсь попытаться процитировать экстенсивно из собственной документации Apples для следующих бит.
Запуск цикла очень похож на его название. Это цикл, в который входит поток, и используется для запуска обработчиков событий в ответ на входящие события. Ваш код предоставляет управляющие операторы, используемые для реализации фактической части цикла цикла выполнения, другими словами, ваш код предоставляет цикл while
или for
, который управляет циклом запуска. В вашем цикле вы используете объект цикла цикла для "запуска" кода обработки событий, который принимает события и вызывает установленные обработчики.
Анатомия цикла запуска - Руководство по программированию потоков - developer.apple.com
CADisplayLink
использует NSRunLoop
и должен быть добавлен к одному, но отличается. Чтобы процитировать заголовочный файл CADisplayLink:
"Если не будет приостановлено, он будет запускать каждый vsync до удаления".
От: func add(to runloop: RunLoop, forMode mode: RunLoopMode)
И из документации свойств preferredFramesPerSecond
.
Значение по умолчанию равно нулю, что означает, что ссылка на изображение будет срабатывать при нативной каденции аппаратного обеспечения дисплея.
...
Например, если максимальная частота обновления экрана составляет 60 кадров в секунду, это также самая высокая частота кадров, которую показывает ссылка на изображение в качестве фактической частоты кадров.
Итак, если вы хотите сделать что-нибудь по времени для обновления экрана CADisplayLink
(с настройками по умолчанию), вы хотите использовать.
Знакомство с сервером рендеринга
Если вам случится блокировать поток, это не имеет никакого отношения к тому, как работает UIKit.
Не совсем. Причина, по которой мы должны касаться только UIViews из основного потока, состоит в том, что UIKit не является потокобезопасным и работает на основном потоке. Если вы блокируете основной поток, вы заблокировали поток, на который выполняется UIKit.
Будет ли UIKit работать "как вы говорите" {... "отправить сообщение, чтобы остановить видеокадры. сделайте всю нашу работу! Отправьте еще одно сообщение, чтобы снова начать видео!" }
Это не то, что я говорю.
Или работает ли "как я говорю" {... то есть, как и в обычном программировании, сделайте столько, сколько сможете, пока кадры не закончатся - о нет, это заканчивается! - подождите, пока следующий кадр! сделайте больше... "}
Это не то, как работает UIKit, и я не понимаю, как это было возможно, без принципиального изменения его архитектуры. Как это означает наблюдать за окончанием кадра?
Как обсуждалось в разделе "Обзор макета и рендеринга UIKit" моего ответа, UIKit пытается не работать заранее. -setNeedsLayout
и -setNeedsDisplay
можно назвать столько раз за кадр, сколько хотите. Они только аннулируют макет и визуализацию рендеринга, если он уже был признан недействительным, что кадр, а второй вызов ничего не делает. Это означает, что если 10 изменений все недействительны, то макет представления UIKit по-прежнему должен оплачивать только однократное вычисление раскладки (если вы не использовали -layoutIfNeeded
между вызовами -setNeedsLayout
).
То же самое относится к -setNeedsDisplay
. Хотя, как обсуждалось ранее, ни одно из них не относится к тому, что появляется на экране. layoutIfNeeded
обновляет кадр представления, а displayIfNeeded
обновляет текстуру рендеринга, но это не связано с тем, что появляется на экране. Представьте, что каждый UIView имеет переменную UIImage, которая представляет собой хранилище поддержки (фактически на CALayer, или ниже, и не является UIImage, но это иллюстрация). Повторное изменение этого вида просто обновляет UIImage. Но UIImage - это всего лишь данные, а не графические изображения на экране, пока что-то не нарисовано на экране.
Итак, как сделать UIView на экране?
Раньше я писал псевдокод UIKits main render runloop. До сих пор в моих ответах я игнорировал значительную часть UIKit, но не все это работает внутри вашего процесса. Удивительное количество вещей UIKit, связанных с отображением вещей, на самом деле происходит в процессе сервера рендеринга, а не в ваших приложениях. Сервер сервера/окна рендеринга был SpringBoard (пользовательский интерфейс для домашнего экрана) до iOS 6 (с тех пор BackBoard и FrontBoard поглотили множество SpringBoards более функциональными функциями, связанными с ОС, в результате чего он больше фокусировался на главном пользовательском интерфейсе операционной системы. экран/блокировка экрана/центр уведомлений/центр управления/переключатель приложений/и т.д.).
Псевдокод для основной панели рендеринга UIKits, скорее всего, ближе к этому. И опять же, помните, что архитектура UIKits предназначена для того, чтобы сделать как можно меньше работы, поэтому она будет делать это только один раз за кадр (в отличие от сетевых вызовов или что-то еще, что может управлять основной runloop).
-(void)runloop
{
//... do touch handling and other events, etc...
UIWindow.allWindows.layoutIfNeeded() // effectively call layoutIfNeeded on everything
UIWindow.allWindows.recursivelyDisplay() // call -drawRect: on things that need to be rerendered
// Sends all the changes to the render server process to actually make these changes appear on screen
// CATransaction.flush() which means:
CoreAnimation.commit_layers_animations_to_WindowServer()
}
Это имеет смысл, одно замораживание приложения iOS не должно блокировать все устройство. На самом деле мы можем продемонстрировать это на iPad с двумя приложениями, работающими бок о бок. Когда мы заставляем замораживать, другое не затрагивается.
![]()
Это два пустых шаблона приложений, которые я создал, и вставлял один и тот же код в оба. Оба должны иметь текущее время в метке в середине экрана. Когда я нажимаю кнопку "заморозить", он вызывает sleep(1)
и зависает от приложения. Все останавливается. Но iOS в целом все в порядке. Другие приложения, центр управления, центр уведомлений и т.д. Все это не подвержено влиянию.
Будет ли UIKit работать "как вы говорите" {... "отправить сообщение, чтобы остановить видеокадры. сделайте всю нашу работу! Отправьте еще одно сообщение, чтобы снова начать видео!" }
В приложении нет команды UIKit stop video frames
, потому что ваше приложение вообще не имеет контроля над экраном. Экран будет обновляться с 60FPS, используя любой фрейм, который дает сервер окон. Окно-сервер будет создавать новый кадр для отображения на 60FPS, используя последние известные позиции, текстуры и деревья слоев, с которыми ваше приложение дало ему работать.
Когда вы замораживаете основной поток в своем приложении, строка CoreAnimation.commitLayersAnimationsToWindowServer()
, которая работает последней (после вашего дорогого кода add lots of views
), блокируется и не запускается. В результате, даже если есть изменения, сервер окон еще не был отправлен, и поэтому он просто продолжает использовать последнее состояние, отправленное для вашего приложения.
Анимация - это еще одна часть UIKit, которая заканчивается процессом, на сервере окон. Если перед sleep(1)
в этом примере приложения мы начнем анимацию UIView, мы увидим, что она запустится, тогда метка замерзнет и перестанет обновляться (поскольку sleep()
запущен). Однако, несмотря на то, что основной поток приложений заморожен, анимация будет продолжаться независимо.
func freezePressed() {
var newFrame = animationView.frame
newFrame.origin.y = 600
UIView.animate(withDuration: 3, animations: { [weak self] in
self?.animationView.frame = newFrame
})
// Wait for the animation to have a chance to start, then try to freeze it
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
NSLog("Before freeze");
sleep(2) // block the main thread for 2 seconds
NSLog("After freeze");
}
}
Это результат:
![]()
На самом деле мы можем пойти лучше.
Если мы изменим метод freezePressed()
на это.
func freezePressed() {
var newFrame = animationView.frame
newFrame.origin.y = 600
UIView.animate(withDuration: 4, animations: { [weak self] in
self?.animationView.frame = newFrame
})
// Wait for the animation to have a chance to start, then try to freeze it
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
// Do a lot of UI changes, these should completely change the view, cancel its animation and move it somewhere else
self?.animationView.backgroundColor = .red
self?.animationView.layer.removeAllAnimations()
newFrame.origin.y = 0
newFrame.origin.x = 200
self?.animationView.frame = newFrame
sleep(2) // block the main thread for 2 seconds, this will prevent any of the above changes from actually taking place
}
}
Теперь без вызова sleep(2)
анимация будет работать в течение 0,2 секунды, после чего она будет отменена, и представление будет перемещено в другую часть экрана другого цвета. Тем не менее, вызов сна блокирует основной поток в течение 2 секунд, что означает, что ни одно из этих изменений не отправляется на оконный сервер до тех пор, пока большая часть пути через анимацию не будет.
![]()
И только для подтверждения здесь результат с вычеркнутой строкой sleep()
.
![]()
Это, надеюсь, объяснит, что происходит. Эти изменения похожи на UIViews, которые вы добавляете в свой вопрос. Они помещаются в очередь, чтобы быть включенными в следующее обновление, но поскольку вы блокируете основной поток, отправляя так много за один раз, вы прекращаете отправку сообщения, которое будет включать их в следующий кадр. Следующий кадр не блокируется, iOS создаст новый фрейм, в котором будут показаны все обновления, полученные от SpringBoard, и другое приложение iOS. Но поскольку ваше приложение по-прежнему блокирует свой основной поток, iOS не получает никаких обновлений из вашего приложения и поэтому не будет показывать никаких изменений (если у него нет изменений, например анимаций, уже поставленных на очередь на оконном сервере).
Итак, чтобы суммировать
- UIKit пытается сделать как можно меньше, поэтому пакеты меняют макет и выводят в один ход.
- UIKit работает в основном потоке, блокирование основного потока предотвращает выполнение UIKit до тех пор, пока эта операция не завершится.
- UIKit в процессе не может касаться дисплея, он отправляет слои и обновления на сервер окна каждый кадр
- Если вы блокируете основной поток, изменения никогда не отправляются на оконный сервер и поэтому отображается arent