如何简单地等待iOS中的任何布局?

在开始之前请注意,这与后台处理无关。 没有涉及到背景的“计算”。

只有UIKit

view.addItemsA() view.addItemsB() view.addItemsC() 

让我们来说一个6s的iPhone

这些都需要一秒钟的UIKit来构build。

这将发生:

一大步

他们全部都在一起。 重复一遍,UIKit执行大量工作时,屏幕只会挂起3秒钟。 然后他们都立即出现。

但是让我们说我想要这样的事情发生:

在这里输入图像说明

他们看起来不断进步。 屏幕挂起1秒,而UIKitbuild立一个。 它出现。 它再次挂起,而build立下一个。 它出现。 等等。

(注意“一秒钟”只是一个简单的例子,为了更清楚起见,请看这篇文章的最后一个例子。)

你如何在iOS中做到这一点?

你可以尝试以下。 这似乎并不奏效

 view.addItemsA() view.setNeedsDisplay() view.layoutIfNeeded() view.addItemsB() 

你可以试试这个:

  view.addItemsA() view.setNeedsDisplay() view.layoutIfNeeded()_b() delay(0.1) { self._b() } } func _b() { view.addItemsB() view.setNeedsDisplay() view.layoutIfNeeded() delay(0.1) { self._c() }... 
  • 请注意,如果值太小 – 这种方法很简单,显然,什么都不做 。 UIKit将继续工作。 (还有什么呢?) 如果价值太大,这是毫无意义的。

  • 请注意,目前(iOS10), 如果我没有弄错 :如果你试着用零延迟的技巧来试试这个技巧,那么它的工作原理是不正确的。 (正如你可能期望的那样)

跳过运行循环…

 view.addItemsA() view.setNeedsDisplay() view.layoutIfNeeded() RunLoop.current.run(mode: .defaultRunLoopMode, before: Date()) view.addItemsB() view.setNeedsDisplay() view.layoutIfNeeded() 

合理。 但是我们最近的真实生活testing表明,这在许多情况下似乎不起作用。

(例如,苹果的UIKit现在已经足够复杂,可以将UIKit的工作涂抹得超出“诀窍”。)

思考:在UIKit中,有没有一种方法可以在callback的时候获得callback,基本上可以绘制出堆叠起来的所有视图? 还有其他解决scheme吗?

一个解决scheme似乎是..把子视图控制器 ,所以你得到一个“didAppear”callback,并跟踪这些。 这似乎是婴儿 ,但也许这是唯一的模式? 无论如何,它真的会工作吗? (只有一个问题:我看不到任何保证,确保所有的子视图都被绘制出来。)


如果这仍然不清楚…

示例日常使用案例:

•说大概有七个部分

•说每个人通常需要0.01到0.20 UIKit构造(取决于你显示的信息)。

•如果你只是“让整个事情在一个重击中”,它通常是可以接受的(总时间,比如0.05到0.15)…但是…

•“出现新屏幕”时,用户经常会感到乏味的停顿。 ( .1至.5或更差 )。

•如果你按照我的要求去做,它会一直平滑到屏幕上,每次只有一个块,每个块都有最less的时间。

窗口服务器对屏幕上出现的内容有最终的控制。 当提交当前CATransaction时,iOS仅向窗口服务器发送更新。 为了在需要时做到这一点,iOS为主线程的运行循环上的.beforeWaiting活动注册一个CFRunLoopObserver 。 在处理一个事件(大概是通过调用你的代码)之后,运行循环在等待下一个事件到来之前调用观察者。 观察者提交当前的交易,如果有的话。 提交事务包括运行布局过程,显示过程(在其中调用drawRect方法),并将更新的布局和内容发送到窗口服务器。

如果需要,调用layoutIfNeeded执行布局,但不会调用显示通道或向窗口服务器发送任何内容。 如果您希望iOS将更新发送到窗口服务器,则必须提交当前事务。

一种方法是调用CATransaction.flush() 。 使用CATransaction.flush()一个合理的例子是,当你想在屏幕上放置一个新的CALayer ,并且你希望它立即有一个animation。 新的CALayer将不会被发送到窗口服务器,直到交易提交,并且您不能添加animation,直到它在屏幕上。 所以,将图层添加到图层层次结构中,调用CATransaction.flush() ,然后将animation添加到图层。

您可以使用CATransaction.flush获取所需的效果。 我不推荐这个 ,但是这是代码:

 @IBOutlet var stackView: UIStackView! @IBAction func buttonWasTapped(_ sender: Any) { stackView.subviews.forEach { $0.removeFromSuperview() } for _ in 0 ..< 3 { addSlowSubviewToStack() CATransaction.flush() } } func addSlowSubviewToStack() { let view = UIView() // 300 milliseconds of “work”: let endTime = CFAbsoluteTimeGetCurrent() + 0.3 while CFAbsoluteTimeGetCurrent() < endTime { } view.translatesAutoresizingMaskIntoConstraints = false view.heightAnchor.constraint(equalToConstant: 44).isActive = true view.backgroundColor = .purple view.layer.borderColor = UIColor.yellow.cgColor view.layer.borderWidth = 4 stackView.addArrangedSubview(view) } 

结果如下:

CATransaction.flush演示

上述解决scheme的问题是通过调用Thread.sleep来阻塞主线程。 如果你的主线程不响应事件,用户不仅会感到沮丧(因为你的应用程序没有响应她的触摸),但最终iOS会决定应用程序被挂起并杀死它。

更好的方法是简单地安排添加每个视图,当你想要它出现。 你声称“这不是工程学”,但你错了,你给出的理由是没有意义的。 iOS通常每16⅔毫秒更新一次屏幕(除非你的应用需要比处理事件更长的时间)。 只要你想要的延迟时间至less是这么长,你可以安排一个块在延迟后运行,以添加下一个视图。 如果你想要一个小于16⅔毫秒的延迟,你一般不能拥有它。

所以这里是更好,推荐的方式来添加子视图:

 @IBOutlet var betterButton: UIButton! @IBAction func betterButtonWasTapped(_ sender: Any) { betterButton.isEnabled = false stackView.subviews.forEach { $0.removeFromSuperview() } addViewsIfNeededWithoutBlocking() } private func addViewsIfNeededWithoutBlocking() { guard stackView.arrangedSubviews.count < 3 else { betterButton.isEnabled = true return } self.addSubviewToStack() DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(300)) { self.addViewsIfNeededWithoutBlocking() } } func addSubviewToStack() { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false view.heightAnchor.constraint(equalToConstant: 44).isActive = true view.backgroundColor = .purple view.layer.borderColor = UIColor.yellow.cgColor view.layer.borderWidth = 4 stackView.addArrangedSubview(view) } 

这是相同的结果:

DispatchQueue asyncAfter演示

TLDR

使用CATransaction.flush()将待处理的UI更改强制到渲染服务器上,或者使用CADisplayLink (下面的示例代码)将工作分成多个帧。


概要

UIKit中有没有一种方法可以在绘制所有堆叠的视图时进行callback?

没有

iOS的作用就像游戏渲染的变化(不pipe你做了多less次)每帧最多一次。 在屏幕上显示更改后,保证代码运行的唯一方法是等待下一帧。

还有其他解决scheme吗?

是的,iOS可能只能渲染每帧一次的变化,但是你的应用不是那个渲染的东西。 窗口服务器进程是。
您的应用程序执行其布局和渲染,然后将其更改提交到其layerTree到渲染服务器。 它将在runloop结束时自动完成,或者可以强制未完成的事务发送到渲染服务器,调用CATransaction.flush()

但是,阻塞主线程一般是不好的(不仅仅是因为它阻塞了UI更新)。 所以,如果你能,你应该避免它。

可能的解决scheme

这是你感兴趣的部分。

1:尽可能多地在后台队列上尽可能地提高性能。
认真的iPhone 7是我家的第三强function电脑 (不是电话),只有我的游戏PC和Macbook Pro才能打败。 它比我家里的其他电脑都快 它不应该停留3秒来呈现您的应用程序用户界面。

2:刷新挂起的CAT交易
编辑:正如roboff mayoff所指出的那样,你可以强制CoreAnimation通过调用CATransaction.flush()将未决的改变发送到渲染服务器。

 addItems1() CATransaction.flush() addItems2() CATransaction.flush() addItems3() 

这实际上不会在那里显示更改,而是将未完成的UI更新发送到窗口服务器,确保它们包含在下一个屏幕更新中。
这将起作用,但在Apples文档中提供了这些警告。

但是,您应该尝试避免显式调用flush。 通过允许flush在runloop期间执行……以及从事务到事务的事务和animation将继续起作用。

然而, CATransaction头文件包含这个引用,这似乎暗示,即使他们不喜欢它,这也是官方支持的用法。

在某些情况下(即没有运行循环或运行循环被阻塞),可能有必要使用显式事务来及时获取渲染树更新。

苹果的文档 – “更好的文档+ + [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() } } 

你在你的回答中提到这不适合你。 但是,它在testingSwift 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's not needed displayLink.isPaused = (itemQueue.count == 0) } } private var itemQueue:[()->()] = [] func addItem(block:@escaping ()->()) { displayLink.isPaused = false // It's needed again itemQueue.append(block) // Add the closure to the queue } } 

5:直接调用runloop。
我不喜欢它,因为有无限循环的可能性。 但是,我承认这不太可能。 我也不确定这是官方支持还是苹果工程师要读这个代码,看起来很恐怖。

 // 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)


我应该使用哪个选项?

我喜欢解决schemeDispatchQueue.main.asyncAfter(deadline: .now() + 0.0)CADisplayLink最好的。 但是, DispatchQueue.main.asyncAfter不能保证它会运行在下一个runloop打勾,所以你可能不想相信它?

CATransaction.flush()将强制用户界面的变化被推送到渲染服务器,这种用法似乎符合苹果对该类的评论,但附带一些警告。

在某些情况下(即没有运行循环或运行循环被阻塞),可能有必要使用显式事务来及时获取渲染树更新。


详细说明

这个答案的其余部分是背景UIKit里面发生了什么,并解释了为什么原始的答案尝试使用view.setNeedsDisplay()view.layoutIfNeeded()没有做任何事情。


UIKit布局和渲染概述

CADisplayLink与UIKit和runloop完全无关。

不完全的。 iOS的UI是像3D游戏一样呈现GPU。 并尽量less做。 所以很多东西,比如布局和渲染,当某些东西发生变化,但是在需要的时候,就不会发生。 这就是为什么我们调用“setNeedsLayout”而不是布局子视图。 每个框架的布局可能会改变多次。 但是,iOS将尝试每帧调用layoutSubviews一次,而不是可能调用了10次setNeedsLayout

然而,在CPU(layout, -drawRect:等等)上发生了很多事情,所以它们如何组合在一起。

请注意,这是所有简化,并跳过许多事情,如CALayer实际上是真正的视图对象,显示在屏幕上而不是UIView等…

每个UIView可以被认为是一个位图,一个图像/ GPU纹理。 渲染屏幕时,GPU将视图层次结构合并到我们看到的结果帧中。 它构成了视图,将子视图纹理渲染到我们在屏幕上看到的完成的渲染(类似于游戏)。

这就是iOS允许拥有这样一个stream畅且容易animation的界面。 要在整个屏幕上对视图进行animation处理,不必重新渲染任何内容。 在视图纹理的下一帧,只是在屏幕上稍微不同的地方合成。 也不是这个观点,而是需要把内容重新排列。

在过去,一个常见的性能提示过去是通过完全在drawRect:渲染表格视图单元来减less视图层次结构中的视图数量。 这个技巧是为了让早期iOS设备的GPU堆肥步骤更快。 然而,现代iOS设备上的GPU速度非常快,现在已经不再担心了。

LayoutSubviews和DrawRect

-setNeedsLayout使视图当前布局无效,并将其标记为需要布局。
如果没有有效的布局, -layoutIfNeeded将重新布局视图

-setNeedsDisplay会将视图标记为需要重绘。 我们之前说过,每个视图都被渲染成视图的纹理/图像,可以被GPU移动和操作,而不需要重绘。 这将触发它重绘。 绘图是通过在CPU上调用-drawRect:来完成的,所以比能够依赖GPU的速度慢,它可以完成大多数帧。

而重要的是要注意的是这些方法不做。 布局方法不做任何可视化的事情。 尽pipe如果视图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。 在这种情况下,它为视图创build了一个单独的纹理,它的子视图和渲染(在GPU上考虑)视图和它的子视图到一个单一的纹理,有效地caching每帧必须做的合成。 这具有不需要每帧构成其所有子视图的性能优点。 但是,如果视图或其子视图频繁更改(如在animation过程中),则会导致性能损失,因为它会频繁地使caching的纹理无效,需要重绘(与频繁调用-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次变化都会一次显示出来,而不是一次一次显示出来呢?

那么,如果这个代码是由于按下button或类似的button而运行的,那么它正在执行同步阻塞主线程(线程UIKit需要UI更改),因此阻塞了第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 

这将工作,只要它不经常使用,因为这是使用recursion。 如果频繁使用,每次调用-runloop都可能触发另一个导致失控recursion的调用。


结束


在这一点下面是澄清。


关于这里发生了什么的额外信息


CADisplayLink和NSRunLoop

如果我没有弄错KH,从根本上说,你认为“运行循环”(即:这个:RunLoop.current)是CADisplayLink。

runloop和CADisplayLink不是一回事。 但是CADisplayLink被附加到一个runloop为了工作。
当我说NSRunLoop每次打勾调用CADisplayLink的时候,我稍微有点误会(在聊天中),事实并非如此。 据我的理解,NSRunLoop基本上是一个while(1)循环,它的工作是保持线程活着,处理事件等。为了避免滑倒,我将尝试从苹果公司自己的文档中大量引用下面的内容。

运行循环非常像它的名字的声音。 这是一个循环,你的线程进入并用来运行事件处理程序来响应传入的事件。 您的代码提供了用于实现运行循环的实际循环部分的控制语句 – 换句话说,您的代码提供了驱动运行循环的whilefor循环。 在循环中,使用运行循环对象来“运行”接收事件的事件处理代码,并调用已安装的处理程序。
运行循环parsing – 线程编程指南 – developer.apple.com

CADisplayLink使用NSRunLoop ,需要添加到一个,但是是不同的。 引用CADisplayLink头文件:

“除非暂停,否则它会触发每个vsync,直到删除。”
从: func add(to runloop: RunLoop, forMode mode: RunLoopMode)

并从preferredFramesPerSecond属性文档。

默认值为零,这意味着显示链接将以显示硬件的本地节奏触发。

例如,如果屏幕的最大刷新率是每秒60帧,那么也是显示链路设置为实际帧速率的最高帧速率。

所以,如果你想做任何事情来屏幕刷新CADisplayLink (默认设置)是你想使用的。

介绍渲染服务器

如果你碰巧阻塞了一个线程,这与UIKit的工作方式没有任何关系。

不完全的。 我们只需要在主线程中触摸UIView的原因是因为UIKit不是线程安全的,它在主线程上运行。 如果阻塞主线程,则阻塞了UIKit运行的线程。

是否UIKit的作品“就像你说的”{…“发送消息停止video帧,做我们所有的工作!发送另一个消息,再次开始video!

这不是我要说的。

或者它是否像我所说的那样起作用… …就像正常的编程一样,尽可能的去做,直到帧结束 – 哦,没有结束 – 等到下一帧! }

这并不是UIKit的工作原理,如果不从根本上改变其架构,我也不会看到它是如何实现的。 如何观看帧的结尾?

正如我的答案UIKit的“UIKit布局和渲染概述”部分所讨论的,UIKit尝试不做任何工作。 -setNeedsLayout-setNeedsDisplay可以根据需要每帧多次调用。 它们只会使布局和视图渲染无效,如果已经使该帧无效,则第二次调用什么也不做。 这意味着,如果10次更改全部使视图的布局无效UIKit仍然只需支付一次重新计算布局的成本(除非在-setNeedsLayout调用之间使用了-layoutIfNeeded )。

-setNeedsDisplay也是如此。 尽pipe如前所述,这些都不涉及屏幕上显示的内容。 layoutIfNeeded更新视图框架和displayIfNeeded更新视图渲染纹理,但这与屏幕上显示的内容无关。 想象一下,每个UIView都有一个UIImagevariables来表示它的后台存储(它实际上是在CALayer中,或者在下面,而不是UIImage,但是这是一个例子)。 重绘该视图只需更新UIImage。 但是UIImage仍然只是数据,而不是屏幕上的graphics,直到它被某种东西吸引到屏幕上。

那么UIView如何在屏幕上绘制?

早些时候,我写了伪代码UIKit的主渲染runloop。 到目前为止,在我的回答中,我忽视了UIKit的一个重要部分,并不是所有的部分都在你的过程中运行。 UIKit与显示东西相关的东西的数量惊人的实际发生在渲染服务器进程而不是你的应用程序进程。 渲染服务器/窗口服务器是SpringBoard(主屏幕用户界面),直到iOS 6(从那时起,BackBoard和FrontBoard吸收了许多SpringBoard的核心操作系统相关的function,使其更专注于作为主要的操作系统UI。屏幕/locking屏幕/通知中心/控制中心/应用程序切换器/等…)。

UIKit主渲染runloop的伪代码可能更接近于此。 再次记住,UIKit的架构devise的目的是尽可能减less工作量,所以每帧只能做一次这样的事情(不像networking调用或者主要的runloop也可以pipe理)。

 -(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上展示这个应用程序,并且两个应用程序并排运行。 当我们导致一个冻结另一个不受影响。

这些是我创build的两个空的应用程序模板,并将相同的代码粘贴到两个模板中。 屏幕中间的标签都应该是当前时间。 当我按下冻结它调用sleep(1)并冻结应用程序。 一切都停止了 但整体而言,iOS是好的。 另一个应用程序,控制中心,通知中心等都不受影响。

是否UIKit的作品“就像你说的”{…“发送消息停止video帧,做我们所有的工作!发送另一个消息,再次开始video!

在应用程序中没有UIKit stop video frames命令,因为你的应用程序根本无法控制屏幕。 屏幕将使用窗口服务器提供的任何帧以60FPS更新。 窗口服务器将使用您的应用程序为其提供的最后已知位置,纹理和图层树,以60FPS的速度合成一个新的显示帧。
当您冻结应用程序中的主线程时,最后运行的CoreAnimation.commitLayersAnimationsToWindowServer()行(在您add lots of views代码之后)将被阻止,并且不会运行。 因此,即使有变化,窗口服务器还没有发送它们,所以只是继续使用它发送给你的应用程序的最后一个状态。

animation是UIKit的另一个部分,在窗口服务器中用完。 如果在该示例应用程序的sleep(1)之前,我们首先启动一个UIViewanimation,然后标签将冻结并停止更新(因为sleep()已经运行)。 但是,即使应用主线程被冻结,animation也将继续。

 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)调用animation将运行0.2秒,那么它将被取消,视图将被移动到屏幕的不同部分不同的颜色。 但是,睡眠呼叫会阻塞主线程2秒,这意味着这些更改都不会发送到窗口服务器,直到通过animation的大部分时间。

这里只需要确认sleep()行的结果。

这应该有希望解释发生了什么事情。 这些更改就像您在问题中添加的UIView。 他们排队等待下一次更新,但是因为你一次发送了很多信息就阻塞了主线程,所以你正在停止发送的消息,这将使它们包含在下一个帧中。 下一帧没有被阻挡,iOS会产生一个新的帧,显示从SpringBoard和其他iOS应用程序收到的所有更新。 But because your app is still blocking it's main thread iOS hasn't received any updates from your app and so won't show any change (unless it has changes, like animations, already queued up on the window server).

So to summarise

  • UIKit tries to do as little as possible so batches changes to layout and rendering up into one go.
  • UIKit runs on the main thread, blocking the main thread prevents UIKit doing anything until that operation has completed.
  • UIKit in process can't touch the display, it sends layers and updates to the window server every frame
  • If you block the main thread then the changes are never sent to the window server and so aren't displayed

It is kind of a solution. But it's not engineering.

Actually, yes it is. By adding the delay, you are doing exactly what you said you wanted to do: you are permitting the runloop to complete and layout to be performed, and re-entering on the main thread as soon as that's done. That, in fact, is one of my main uses of delay . (You might even be able to use a delay of zero.)

3 methods that might work below. The first I could make it work if a subview is adding the controller as well if it is not directly in the view controller.The second is a custom view 🙂 It seems you are wondering when layoutSubviews is finished on the view to me. This continuous process is what is freezing the display because of the 1000 subviews plus sequentially. Depending on your situation you can add a childviewcontroller view and post a notification when viewDidLayoutSubviews() is finished but I don't know if this fits your use case. I tested with 1000 subs on the viewcontroller view being added and it worked. In that case a delay of 0 will do exactly what you want. Here is a working example.

  import UIKit class TrackingViewController: UIViewController { var layoutCount = 0 override func viewDidLoad() { super.viewDidLoad() // Add a bunch of subviews for _ in 0...1000{ let view = UIView(frame: self.view.bounds) view.autoresizingMask = [.flexibleWidth,.flexibleHeight] view.backgroundColor = UIColor.green self.view.addSubview(view) } } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() print("Called \(layoutCount)") if layoutCount == 1{ //finished because first call was an emptyview NotificationCenter.default.post(name: NSNotification.Name(rawValue: "kLayoutFinished"), object: nil) } layoutCount += 1 } } 

Then in your main View Controller that you are adding subviews you could do this.

 import UIKit class ViewController: UIViewController { var y :CGFloat = 0 var count = 0 override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view, typically from a nib. NotificationCenter.default.addObserver(self, selector: #selector(ViewController.finishedLayoutAddAnother), name: NSNotification.Name(rawValue: "kLayoutFinished"), object: nil) } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 4, execute: { //add first view self.finishedLayoutAddAnother() }) } deinit { NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: "kLayoutFinished"), object: nil) } func finishedLayoutAddAnother(){ print("We are finished with the layout of last addition and we are displaying") addView() } func addView(){ // we keep adding views just to cause print("Fired \(Date())") if count < 100{ DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.0, execute: { // let test = TestSubView(frame: CGRect(x: self.view.bounds.midX - 50, y: y, width: 50, height: 20)) let trackerVC = TrackingViewController() trackerVC.view.frame = CGRect(x: self.view.bounds.midX - 50, y: self.y, width: 50, height: 20) trackerVC.view.backgroundColor = UIColor.red self.view.addSubview(trackerVC.view) trackerVC.didMove(toParentViewController: self) self.y += 30 self.count += 1 }) } } } 

Or there is an EVEN crazier way and probably better way. Create your own view that in a sense keeps its own time and calls back when it is good to not drop frames. This is unpolished but would work.

completion gif

 import UIKit class CompletionView: UIView { private var lastUpdate : TimeInterval = 0.0 private var checkTimer : Timer! private var milliSecTimer : Timer! var adding = false private var action : (()->Void)? //just for testing private var y : CGFloat = 0 private var x : CGFloat = 0 //just for testing var randomColors = [UIColor.purple,UIColor.gray,UIColor.green,UIColor.green] init(frame: CGRect,targetAction:(()->Void)?) { super.init(frame: frame) action = targetAction adding = true for i in 0...999{ if y > bounds.height - bounds.height/100{ y -= bounds.height/100 } let v = UIView(frame: CGRect(x: x, y: y, width: bounds.width/10, height: bounds.height/100)) x += bounds.width/10 if i % 9 == 0{ x = 0 y += bounds.height/100 } v.backgroundColor = randomColors[Int(arc4random_uniform(4))] self.addSubview(v) } } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } func milliSecCounting(){ lastUpdate += 0.001 } func checkDate(){ //length of 1 frame if lastUpdate >= 0.003{ checkTimer.invalidate() checkTimer = nil milliSecTimer.invalidate() milliSecTimer = nil print("notify \(lastUpdate)") adding = false if let _ = action{ self.action!() } } } override func layoutSubviews() { super.layoutSubviews() lastUpdate = 0.0 if checkTimer == nil && adding == true{ checkTimer = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(CompletionView.checkDate), userInfo: nil, repeats: true) } if milliSecTimer == nil && adding == true{ milliSecTimer = Timer.scheduledTimer(timeInterval: 0.001, target: self, selector: #selector(CompletionView.milliSecCounting), userInfo: nil, repeats: true) } } } import UIKit class ViewController: UIViewController { var y :CGFloat = 30 override func viewDidLoad() { super.viewDidLoad() // Wait 3 seconds to give the sim time DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 3, execute: { [weak self] in self?.addView() }) } var count = 0 func addView(){ print("starting") if count < 20{ let completionView = CompletionView(frame: CGRect(x: 0, y: self.y, width: 100, height: 100), targetAction: { [weak self] in self?.count += 1 self?.addView() print("finished") }) self.y += 105 completionView.backgroundColor = UIColor.blue self.view.addSubview(completionView) } } } 

Or Finally,you could do the callback or notification in viewDidAppear but it also seems that any code executed on the callback would need to be wrapped in to execute in a timely manner from the viewDidAppear callback.

 DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.0, execute: { //code}) 

Use NSNotification to achieve needed effect.

First – register an observer to the main view and create observer handler.

Then, initialize all these A,B,C… objects in separate thread (background, for instance) by, for instance, self performSelectorInBackground

Then – post notification from subviews and the last – performSelectorOnMainThread to add subview in desired order with needed delays.
To answer the questions in comments, let's say you have a UIViewController that was shown on the screen. This object – not a point of discussion and you can decide where to put the code, controlling view appearance. The code is for the UIViewController object (so, it is self). View – some UIView object, considered as a parent view. ViewN – one of subviews. It can be scaled later.

 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:@"ViewNotification" object:nil]; 

This registerd an observer – needed to communicate between threads. ViewN * V1 = [[ViewN alloc] init]; Here – subviews can be allocated – not shown yet.

 - (void) handleNotification: (id) note { ViewN * Vx = (ViewN*) [(NSNotification *) note.userInfo objectForKey: @"ViewArrived"]; [self.View performSelectorOnMainThread: @selector(addSubView) withObject: Vx waitUntilDone: FALSE]; } 

This handler allows to receive messages and place UIView object to the parent view. Looks strange, but the point is – you need to execute addSubview method on the main thread to take effect. performSelectorOnMainThread allows to start adding subview on main thread without blocking application execution.

Now – we make a method that will place subviews to the screen.

 -(void) sendToScreen: (id) obj { NSDictionary * mess = [NSDictionary dictionaryWithObjectsAndKeys: obj, @"ViewArrived",nil]; [[NSNotificationCenter defaultCenter] postNotificationName: @"ViewNotification" object: nil userInfo: mess]; } 

This method will post notification from any thread, sending an object as NSDictionary item named ViewArrived.
And finally – views that have to be added with 3 seconds delay:

 -(void) initViews { ViewN * V1 = [[ViewN alloc] init]; ViewN * V2 = [[ViewN alloc] init]; ViewN * V3 = [[ViewN alloc] init]; [self performSelector: @selector(sendToScreen:) withObject: V1 afterDelay: 3.0]; [self performSelector: @selector(sendToScreen:) withObject: V2 afterDelay: 6.0]; [self performSelector: @selector(sendToScreen:) withObject: V3 afterDelay: 9.0]; } 

It is not the only one solution. It is also possible to control subviews of the parent view by counting the NSArray subviews property.
In any case, you can run initViews method whenever you need and even in background thread and it allows to control subview appearance, performSelector mechanism allows to avoid execution thread blocking.