TL; DR
Enable pending user interface changes on the render server using CATransaction.flush() or split the work into several frames using CADisplayLink (sample code below).
Summary
Is there a way, in UIKit, to get a callback when it has drawn all the views you viewed?
No
iOS acts like changing the rendering of a game (no matter how much you make) no more than once per frame. The only way to guarantee the passage of the code after your changes have been displayed on the screen is to wait for the next frame.
Is there any other solution?
Yes, iOS can only process changes once per frame, but your application is not what this rendering does. The window server process.
Your application does its layout and rendering, and then commits its changes to its layerTree on the render server. It will do this automatically at the end of runloop, or you can force pending transactions to the visualization server CATransaction.flush() .
However, blocking the main thread is generally bad (not only because it blocks user interface updates). Therefore, if you can avoid it,
Possible solutions
This is the part that interests you.
1: Make as many background queues as you can and increase productivity.
Seriously, the iPhone 7 is the third most powerful computer (not a phone) in my house, just beaten up by my gaming PC and Macbook Pro. It is faster than every other computer in my house. A pause of 3 seconds is not required to display the application user interface.
2: latent waiting for CAT transactions
EDIT: As rob mayoff pointed out , you can force CoreAnimation to send pending changes to the render server by calling CATransaction.flush()
addItems1() CATransaction.flush() addItems2() CATransaction.flush() addItems3()
This does not actually make a difference, but sends the pending user interface updates to the window server, ensuring that they are included in the next screen update.
This will work, but with this warning in the documentation for Apple.
However, you should try to avoid calling flush explicitly. By providing a flash to execute during runloop ...... both transactions and animations that work from transaction to transaction will continue to function.
However, the CATransaction header file includes this quote, which seems to imply that even if they don't like it, it is officially supported.
In some cases (i.e., no loop cycle or loop cycle is blocked), explicit transactions may be required to receive timely updates to the rendering tree.
Apple Documentation - "Best Documentation for + [CATransaction flush] . "
3: dispatch_after()
Just delay the code until the next runloop. dispatch_async(main_queue) will not work, but you can use dispatch_after() without delay.
addItems1() DispatchQueue.main.asyncAfter(deadline: .now() + 0.0) { addItems2() DispatchQueue.main.asyncAfter(deadline: .now() + 0.0) { addItems3() } }
You mentioned in your answer that this no longer works for you. However, it works great in the test Swift Playground and the iOS app example that I included with this answer.
4: Use CADisplayLink
CADisplayLink is called once for each frame and allows you to ensure that only one operation is performed for each frame, ensuring that the screen can be updated between operations.
DisplayQueue.sharedInstance.addItem { addItems1() } DisplayQueue.sharedInstance.addItem { addItems2() } DisplayQueue.sharedInstance.addItem { addItems3() }
This helper class is required to work (or similar).
// 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:
5: call runloop directly.
I don’t like it because of the possibility of an infinite loop. But I admit, this is unlikely. I'm also not sure if this is officially supported, or an Apple engineer is going to read this code and look horrified.
// 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()
This should work if only (although it doesn’t respond to runloop events) you do something else to block the runloop call from completing, since CATransaction is sent to the window server at the end of runloop.
Code example
Xcode Demo and Xcode Playground (Xcode 8.2, Swift 3)
Which option to use?
I like DispatchQueue.main.asyncAfter(deadline: .now() + 0.0) and CADisplayLink best. However, DispatchQueue.main.asyncAfter does not guarantee that it will work at the next runloop mark so that you would not want to trust it?
CATransaction.flush() will force you to change the user interface settings on the render server, and this use seems to follow Apple's comments on the class, but it contains some warnings.
In some cases (i.e., no loop cycle or loop cycle is blocked), explicit transactions may be required to receive timely updates to the rendering tree.
Detailed explanation
The rest of this answer is a background to what goes on inside UIKit, and explains why the original answers to attempts to use view.setNeedsDisplay() and view.layoutIfNeeded() did nothing.
UIKit Layout and Render Overview
CADisplayLink is not fully affiliated with UIKit and runloop.
Not really. iOS UI is a graphics processor created as a 3D game. And trying to do as little as possible. Therefore, many things, such as layout and rendering, do not happen when something changes, but when it is needed. This is why we call setNeedsLayout not mock views. Each frame can change several times. However, iOS will only attempt to call layoutSubviews once per frame, and not 10 times that setNeedsLayout can be called.
However, a lot happens on the processor (layout, -drawRect: etc.), since all this is combined.
Please note that this is all simplified and misses a lot of things, such as CALayer, which is actually a real viewer that does not display a UIView, etc.
Each UIView can be thought of as a bitmap, texture image / GPU. When the screen is displayed, the GPU combines the presentation hierarchy into the resulting frame that we see. It forms representations, turning subviews textures on top of previous views into a ready-made render that we see on the screen (similar to a game).
This is what enabled iOS to have such a smooth and easily animated interface. To animate a view across the screen, it does not need to rewrite anything. In the next frame, which views the texture, it is simply composed in a slightly different place on the screen than before. Neither he nor the opinion that he was in excess so that their contents were overwritten.
In the past, general performance advice has usually been to reduce the number of views in the view hierarchy by fully displaying view cells in drawRect: This tip was to make the GPU composting step faster on early iOS devices. However, the GPU runs so fast on modern iOS devices that it no longer bothers.
LayoutSubviews and DrawRect
-setNeedsLayout nullifies the current layout of the view and marks it as a necessary layout.
-layoutIfNeeded will pass the view if it does not have a valid layout
-setNeedsDisplay will mark the views that need to be redrawn. We said earlier that each view is rendered into a texture / image view that can be moved and manipulated using the GPU without the need to redraw. This will cause it to be redrawn. The drawing is done by calling -drawRect: on the CPU and therefore slower than being able to rely on the GPU, which can handle most frames.
And it is important to note that these methods do not. Layout methods do nothing visually. Although if the contentMode set to redraw , changing the view frame may invalidate the rendering of the views (trigger -setNeedsDisplay ).
You can try the following all day long. This does not seem to work:
view.addItemsA() view.setNeedsDisplay() view.layoutIfNeeded() view.addItemsB() view.setNeedsDisplay() view.layoutIfNeeded() view.addItemsC() view.setNeedsDisplay() view.layoutIfNeeded()
From what we learned, the answer should be obvious why this is not working now. view.layoutIfNeeded() does nothing but recalculate the frames of its subzones.
view.setNeedsDisplay() just marks the view as it needs to be redrawn the next time. UIKit scans the view hierarchy, updating the view textures to send to the GPU. However, this does not affect the subviews you tried to add.
In your view.addItemsA() example, 100 submenus have been added. These are separate unrelated layers / textures on the GPU until the GPU merges them into the next framebuffer. The only exception is if CALayer shouldRasterize set to true. In this case, he creates a separate texture for the presentation, and he looks at it and visualizes (thinks on the GPU) the presentation, and he peeps into one texture, effectively caching the composition that he will have to perform each frame. This has a performance advantage when it is not necessary to compile all its routines in each frame. However, if the view or its views change frequently (for example, during animation), this will be a penalty for performance, as this will invalidate the cached texture, which often requires redrawing (similar to calling -setNeedsDisplay ).
Now any game engineer will just do it ...
view.addItemsA() RunLoop.current.run(mode: .defaultRunLoopMode, before: Date()) view.addItemsB() RunLoop.current.run(mode: .defaultRunLoopMode, before: Date()) view.addItemsC()
Now it really works.
But why does this work?
Now, -setNeedsLayout and -setNeedsDisplay do not start relaying or redrawing, but instead simply mark the view as necessary. As UIKit prepares to render the next frame, it invokes views with invalid textures or layouts for redrawing or relaying. After everything is ready, he sends the GPU command to the composition and displays a new frame.
So, the main startup loop in UIKit probably looks something like this.
-(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 }
So, back to the source code.
view.addItemsA() // Takes 1 second view.addItemsB() // Takes 1 second view.addItemsC() // Takes 1 second
So, why do all 3 changes appear immediately after 3 seconds instead of one at a time for 1 second?
Well, if this bit of code is triggered by pressing a button or the like, it synchronously locks the main thread (the UIKit thread requires user interface changes) and therefore blocks the execution cycle in line 1, the even processing part. In fact, you make the first line of the runloop method return in 3 seconds.
However, we determined that the layout will not be updated to line 3, individual views will not be displayed until line 4 and no changes are displayed on the screen until the last line of the runloop method, line 5.
The reason why runloop is manually started is because you basically put in a call to the runloop() method. 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.
Not really. , 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 })
This is the result:

.
freezePressed() .
func freezePressed() { var newFrame = animationView.frame newFrame.origin.y = 600 UIView.animate(withDuration: 4, animations: { [weak self] in self?.animationView.frame = newFrame })
sleep(2) 0,2 , , . , 2 , , , .

sleep() .

, , , . UIViews, . , , , , , . , iOS , , SpringBoard, iOS. - , iOS ( , , ).
,
- UIKit , .
- UIKit , UIKit , .
- UIKit ,
- , arent