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

Fat*_*tie 29 performance runloop ios

在开始注意之前,这与后台处理无关.没有"计算"涉及到背景.

只有UIKit.

view.addItemsA()
view.addItemsB()
view.addItemsC()
Run Code Online (Sandbox Code Playgroud)

让我们说6s的iPhone

其中每一个都需要一秒时间才能构建UIKit.

这将发生:

一大步

他们一直都在出现.重复一遍,屏幕只会挂起3秒,而UIKit会做大量的工作.然后他们都出现了.

但是,让我说我希望这种情况发生:

在此输入图像描述

他们一直在进步.当UIKit构建一个屏幕时,屏幕会暂停1秒钟.它出现.它会在构建下一个时再次挂起.它出现.等等.

(注意"一秒钟"只是一个简单的例子.为了更全面的例子,请参阅本文末尾.)

你是如何在iOS中做到的?

您可以尝试以下方法.它似乎不起作用.

view.addItemsA()

view.setNeedsDisplay()
view.layoutIfNeeded()

view.addItemsB()
Run Code Online (Sandbox Code Playgroud)

你可以试试这个:

 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() }...
Run Code Online (Sandbox Code Playgroud)
  • 请注意,如果该值太小 - 这种方法很简单,显然也没有做任何事情.UIKit将继续努力.(它还会做什么?).如果价值太大,那就没有意义了.

  • 请注意,目前(iOS10),如果我没有弄错的话:如果你尝试使用零延迟的技巧,它最好不正常工作.(正如你可能期望的那样.)

跳转运行循环......

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

RunLoop.current.run(mode: .defaultRunLoopMode, before: Date())

view.addItemsB()
view.setNeedsDisplay()
view.layoutIfNeeded()
Run Code Online (Sandbox Code Playgroud)

合理. 但是我们最近的实际测试显示,在许多情况下这似乎不起作用.

(也就是说,Apple的UIKit现在足够精致,可以将UIKit的工作涂抹在那个"技巧"之外.)

思考:在UIKit中,或许有一种方法可以获得一个回调,基本上,它已经绘制了你所堆积的所有视图了吗?还有其他解决方案吗?

一个解决方案似乎是......将子视图放在控制器中,因此您可以获得"didAppear"回调,并跟踪这些回调. 这似乎是婴儿,但也许这是唯一的模式?无论如何它真的会起作用吗?(仅仅是一个问题:我没有看到任何保证,确保确保所有子视图都已被绘制.)


如果这还不清楚......

日常用例示例:

•假设可能有七个部分.

•假设每个人通常需要0.01到0.20来构建UIKit(取决于您显示的信息).

•如果你只是"让一切都搞砸了"它通常会好或可以接受(总时间,比如0.05到0.15)......但......

•当"新屏幕出现"时,用户经常会有一个单调乏味的停顿.(.1到.5或更糟).

•如果您按照我的要求进行操作,它将始终平滑到屏幕上,一次一个块,每个块的最小可能时间.

Kyl*_*lls 18

TLDR

CATransaction.flush()使用CADisplayLink(下面的示例代码)强制挂起的UI更改到渲染服务器上或使用多个帧分割工作.


摘要

在UIKit中,是否有一种方法可以在它绘制了你已堆积的所有视图时获得回调?

没有

iOS的作用就像游戏渲染更改(无论你制作多少)每帧最多一次.在屏幕上呈现更改后,保证代码运行的唯一方法是等待下一帧.

还有其他解决方案吗?

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

但是,阻塞主线程通常很糟糕(不仅仅因为它阻止了UI更新).所以,如果你能,你应该避免它.

可能的解决方案

这是你感兴趣的部分.

1:尽可能在后台队列上尽可能地提高性能.
说真的,iPhone 7是我家中功能最强大的第三台电脑(不是手机),只能被我的游戏PC和Macbook Pro击败.它比我家里的其他电脑都要快.它不应该暂停3秒来呈现您的应用UI.

2:刷新挂起的CATransactions
编辑:正如rob mayoff所指出的,你可以通过调用强制CoreAnimation将挂起的更改发送到呈现服务器CATransaction.flush()

addItems1()
CATransaction.flush()
addItems2()
CATransaction.flush()
addItems3()
Run Code Online (Sandbox Code Playgroud)

这实际上不会在那里呈现更改,而是将挂起的UI更新发送到窗口服务器,确保它们包含在下一个屏幕更新中.
这将有效,但在Apples文档中附带了这些警告.

但是,您应该尝试避免显式调用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()
  }
}
Run Code Online (Sandbox Code Playgroud)

你在答案中提到这对你不再适用了.但是,它在测试Swift Playground和我已经包含在这个答案中的示例iOS应用程序中工作正常.

4:使用CADisplayLink
CADisplayLink每帧调用一次,并允许您确保每帧只运行一个操作,保证屏幕能够在操作之间刷新.

DisplayQueue.sharedInstance.addItem {
  addItems1()
}
DisplayQueue.sharedInstance.addItem {
  addItems2()
}
DisplayQueue.sharedInstance.addItem {
  addItems3()
}
Run Code Online (Sandbox Code Playgroud)

需要这个帮助程序类工作(或类似).

// 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
  }
}
Run Code Online (Sandbox Code Playgroud)

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()
Run Code Online (Sandbox Code Playgroud)

This should work, unless (while responding to the runloop events) you do something else to block that runloop call from completing as the CATransaction's are sent to the window server at the end of the runloop.


Example Code

Demonstration Xcode Project & Xcode Playground (Xcode 8.2, Swift 3)


Which option should I use?

I like the solutions DispatchQueue.main.asyncAfter(deadline: .now() + 0.0) and CADisplayLink the best. However, DispatchQueue.main.asyncAfter doesn't guarantee it will run on the next runloop tick so you might not want to trust it?

CATransaction.flush() will force you UI changes to be pushed to the render server and this usage seems to fit Apple's comments for the class, but comes with some warnings attached.

In some circumstances (i.e. no run-loop, or the run-loop is blocked) it may be necessary to use explicit transactions to get timely render tree updates.


Detailed Explanation

The rest of this answer is is background on what's going on inside UIKit and explains why the original answers attempts to use view.setNeedsDisplay() and view.layoutIfNeeded() didn't do anything.


Overview of UIKit Layout & Rendering

CADisplayLink is totally unrelated to UIKit and the runloop.

Not quite. iOS's UI is GPU rendered like a 3D game. And tries to do as little as possible. So a lot of things, like layout and rendering don't happen when something changes but when it is needed. That is why we call 'setNeedsLayout’ not layout subviews. Each frame the layout might change multiple times. However, iOS will try to only call layoutSubviews once per frame, instead of the 10 times setNeedsLayout might have been called.

However, quite a lot happens on the CPU (layout, -drawRect:, etc...) so how does it all fit together.

Note this is all simplified and skips lots of things like CALayer actually being the real view object that shows on screen not UIView, etc...

Each UIView can be thought of as a bitmap, an image/GPU texture. When the screen is rendered the GPU composites the view hierarchy into the resulting frame we see. It composes the views, rendering the subviews textures over the top of previous views into the finished render that we see on screen (similarly to a game).

This is what has allowed iOS to have such a smooth and easily animated interface. To animate a view across the screen it doesn't have to rerender anything. On the next frame that views texture is just composited in a slightly different place on the screen than before. Neither it, nor the view it was on top of need to have their contents rerendered.

In the past a common performance tip used to be to cut down on the number of views in the view hierarchy by rendering table view cells entirely in drawRect:. This tip was to make the GPU composting step faster on the early iOS devices. However, GPU's are so fast on modern iOS devices now this is no longer worried about very much.

LayoutSubviews and DrawRect

-setNeedsLayout invalidates the views current layout and marks it as needing layout.
-layoutIfNeeded will relayout the view if it doesn't have a valid layout

-setNeedsDisplay will mark the views as needing to be redraw. We said earlier that each view is rendered into a texture/image of the view which can be moved around and manipulated by the GPU without needing to be redrawn. This will trigger it to redraw. The drawing is done by calling -drawRect: on the CPU and so is slower than being able to rely on the GPU, which it can do most frames.

And important thing to notice is what these methods do not do. The layout methods do not do anything visual. Though if the views contentMode is set to redraw, changing the views frame might invalidate the views render (trigger -setNeedsDisplay).

You can try the following all day. It does not seem to work:

view.addItemsA()
view.setNeedsDisplay()
view.layoutIfNeeded()
view.addItemsB()
view.setNeedsDisplay()
view.layoutIfNeeded()
view.addItemsC()
view.setNeedsDisplay()
view.layoutIfNeeded()
Run Code Online (Sandbox Code Playgroud)

From what we've learnt the answer should be obvious why this doesn't work now.
view.layoutIfNeeded() does nothing but recalculate the frames of its subviews.
view.setNeedsDisplay() just marks the view as needing redrawing next time UIKit sweeps through the view hierarchy updating view textures for sending to the GPU. However, is doesn't effect the subviews you tried to add.

In your example view.addItemsA() adds 100 sub views. Those are separate unrelated layers/textures on the GPU until the GPU composites them together into the next framebuffer. The only exception to this is if the CALayer has shouldRasterize set to true. In which case it creates a separate texture for the view and it's sub views and renders (in think on the GPU) the view and it's subviews into a single texture, effectively caching the compositing it would have to do each frame. This has the performance advantage of not needing to compose all its subviews every frame. However, if the view or its subviews change frequently (like during an animation) it will be a performance penalty, as it will invalidate the cached texture frequently requiring it to be redrawn (similar to frequently calling -setNeedsDisplay).


Now, any game engineer would just do this ...

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

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

view.addItemsC()
Run Code Online (Sandbox Code Playgroud)

Now indeed, that seems to work.

But why does it work?

Now -setNeedsLayout and -setNeedsDisplay don't trigger a relayout or redraw but instead just mark the view as needing it. As UIKit comes through preparing to render the next frame it triggers views with invalid textures or layouts to redraw or relayout. After everything is ready it sends tells the GPU to composite and display the new frame.

So the main run 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
}
Run Code Online (Sandbox Code Playgroud)

So back to your original code.

view.addItemsA() // Takes 1 second
view.addItemsB() // Takes 1 second
view.addItemsC() // Takes 1 second
Run Code Online (Sandbox Code Playgroud)

So why do all 3 changes show up at once after 3 seconds instead of one at a time 1 second apart?

Well if this bit of code is running as a result of a button press, or similar, it is executing synchronously blocking the main thread (the thread UIKit requires UI changes be made on) and so blocks the run loop on line 1, the even processing part. In effect, you are making that first line of the runloop method take 3 seconds to return.

However, we have determined that the layout won't update until line 3, the individual views won't be rendered until line 4 and no changes will actually appear on screen until the last line of the runloop method, line 5.

The reason that pumping the runloop manually works is because you are basically inserting a call to the runloop() method. Your method is running as a result of being called from within the runloop function

-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
Run Code Online (Sandbox Code Playgroud)

This will work, as long as it's not used very often because this is using recursion. If it's used frequently every call to the -runloop could trigger another one leading to runaway recursion.


THE END


Below this point is just clarification.


Extra information about what is going on here


CADisplayLink and NSRunLoop

If I'm not mistaken KH it appears that fundamentally you believe "the run loop" (ie: this one: RunLoop.current) is CADisplayLink.

The runloop and CADisplayLink aren't the same thing. But CADisplayLink gets attached to a runloop in order to work.
I slightly misspoke earlier (in the chat) when I said NSRunLoop calls CADisplayLink every tick, It doesn’t. To my understanding NSRunLoop is basically a while(1) loop that’s job is to keep the thread alive, process events, etc... To avoid slipping up I’m going to try to quote extensively from Apple’s own documentation for the next bits.

A run loop is very much like its name sounds. It is a loop your thread enters and uses to run event handlers in response to incoming events. Your code provides the control statements used to implement the actual loop portion of the run loop—in other words, your code provides the while or for loop that drives the run loop. Within your loop, you use a run loop object to "run" the event-processing code that receives events and calls the installed handlers.
Anatomy of a Run Loop - Threading Programming Guide - developer.apple.com

CADisplayLink uses NSRunLoop and needs to be added to one but is different. To quote the CADisplayLink header file:

"Unless paused, it will fire every vsync until removed."
From: func add(to runloop: RunLoop, forMode mode: RunLoopMode)

And from the preferredFramesPerSecond properties documentation.

Default value is zero, which means the display link will fire at the native cadence of the display hardware.
...
For example, if the maximum refresh rate of the screen is 60 frames per second, that is also the highest frame rate the display link sets as the actual frame rate.

So if you want to do anything timed to screen refreshes CADisplayLink (with default settings) is what you want to use.

Introducing the Render Server

If you happen to block a thread, that has nothing to do with how UIKit works.

Not quite. The reason we are required to only touch UIView’s from the main thread is because UIKit is not thread safe and it runs on the main thread. If you block the main thread you have blocked the thread UIKit runs on.

Whether UIKit works "like you say" {... "send a message to stop video frames. do all our work! send another message to start video again!"}

That’s not what I’m saying.

Or whether it works "like I say" {... ie, like normal programming "do as much as you can until the frames about to end - oh no it's ending! - wait until the next frame! do more..."}

That’s not how UIKit works and I don’t see how it ever could without fundamentally changing its architecture. How is it meant to watch for the frame ending?

As discussed in the "Overview of UIKit Layout & Rendering" section of my answer UIKit tries to do no work upfront. -setNeedsLayout and -setNeedsDisplay can be called as many times per frame as you want. They only invalidate the layout and view render, if it has already been invalidated that frame then the second call does nothing. This means that if 10 changes all invalidate the layout of a view UIKit still only needs to pay the cost of recalculating the layout once (unless you used -layoutIfNeeded in between -setNeedsLayout calls).

The same is true of -setNeedsDisplay. Though as previously discussed neither of these relates to what appears on screen. layoutIfNeeded updates the views frame and displayIfNeeded updates the views render texture, but that is not related to what appears on screen. Imagine each UIView has a UIImage variable that represents it’s backing store (it’s actually in CALayer, or below, and isn’t a UIImage. But this is an illustration). Redrawing that view simply updates the UIImage. But the UIImage is still just data, not a graphic on screen until it is drawn onto the screen by something.

So how does a UIView get drawn on screen?

Earlier I wrote pseudo code UIKit’s main render runloop. So far in my answers I have been ignoring a significant part of UIKit, not all of it runs inside your process. A surprising amount of UIKit stuff related to displaying things actually happens in the render server process not your apps process. The render server/window server was SpringBoard (the home screen UI) until iOS 6 (since then then BackBoard and FrontBoard have absorbed a lot of SpringBoards more core OS related features, leaving it to focus more on being the main operating system UI. Home screen/lock screen/notification center/control center/app switcher/etc...).

The pseudo code for UIKit’s main render runloop is likely closer to this. And again, remember UIKit’s architecture is designed to do as little work as possible so it will only do this stuff once per frame (unlike network calls or whatever else the main runloop might also manage).

-(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()
} 
Run Code Online (Sandbox Code Playgroud)

This makes sense, a single iOS app freezing shouldn’t be able to freeze the entire device. In fact we can demonstrate this on an iPad with 2 apps running side by side. When we cause one to freeze the other is unaffected.

These are 2 empty app templates I created and pasted the same code into both. Both should the current time in a label in the middle of the screen. When I press freeze it calls sleep(1) and freezes the app. Everything stops. But iOS as a whole is fine. The other app, control center, notification center, etc... are all unaffected by it.

Whether UIKit works "like you say" {... "send a message to stop video frames. do all our work! send another message to start video again!"}

In the app there is no UIKit stop video frames command because your app has no control over the screen at all. The screen will update at 60FPS using whatever frame the window server gives it. The window server will composite a new frame for the display at 60FPS using the last known positions, textures and layer trees your app gave it to work with.
When you freeze the main thread in your app the CoreAnimation.commitLayersAnimationsToWindowServer() line, which runs last (after your expensive add lots of views code), is blocked and doesn’t run. As a result even if there are changes, the window server hasn’t been sent them yet and so just continues to use the last state it was sent for your app.

Animations is another part of UIKit that runs out of process, in the window server. If, before the sleep(1) in that example app, we start a UIView animation first we will see it start, then the label will freeze and stop updating (because sleep() has run). However, even though the apps main thread is frozen the animation will continue regardless.

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");
    }
}
Run Code Online (Sandbox Code Playgroud)

This is the result:

In fact we can go one better.

If we change the freezePressed() method to this.

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
    }
}
Run Code Online (Sandbox Code Playgroud)

Now without the sleep(2) call the animation will run for 0.2 seconds then it’ll be canceled and the view will be moved to a different part of the screen a different color. However, the sleep call blocks the main thread for 2 seconds meaning none of these changes are sent to the window server until most of the way through the animation.

And just to confirm here is the result with the sleep() line commented out.

This should hopefully explain what’s going on. These changes are like the UIView’s you add in your question. They are queued up to be included in the next update, but because you are blocking the main thread by sending so many in one go you are stopping the message being sent which will get them included in the next frame. The next frame isn’t being blocked, iOS will produce a new frame showing all the updates it has received from SpringBoard, and other iOS app. 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


rob*_*off 10

窗口服务器可以最终控制屏幕上显示的内容.iOS仅在CATransaction提交当前时向窗口服务器发送更新.为了在需要时实现这一点,iOS会在主线程的运行循环中CFRunLoopObserver.beforeWaiting活动注册一个.在处理事件之后(可能是通过调用代码),run循环在等待下一个事件到达之前调用观察者.如果存在,则观察者提交当前事务.提交事务包括运行布局传递,显示传递(drawRect调用方法),以及将更新的布局和内容发送到窗口服务器.

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

一种方法是打电话CATransaction.flush().一个合理的使用方法CATransaction.flush()是当你想CALayer在屏幕上放一个新的并且你希望它立即有一个动画.CALayer在事务提交之前,新的不会被发送到窗口服务器,并且您无法在屏幕上添加动画.因此,您将图层添加到图层层次结构中,调用CATransaction.flush(),然后将动画添加到图层.

您可以使用它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)
}
Run Code Online (Sandbox Code Playgroud)

这是结果:

CATransaction.flush演示

上述解决方案的问题在于它通过调用来阻塞主线程Thread.sleep.如果您的主线程没有响应事件,不仅用户感到沮丧(因为您的应用程序没有响应她的触摸),但最终iOS将决定应用程序挂起并将其终止.

更好的方法是在您希望它出现时安排添加每个视图.你声称"这不是工程",但你错了,你的理由没有意义.iOS通常每16⅔毫秒更新一次屏幕(除非您的应用程序需要的时间比处理事件的时间长).只要您想要的延迟至少是那么长,您就可以在延迟之后安排一个块来添加下一个视图.如果你想要一个小于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)
}
Run Code Online (Sandbox Code Playgroud)

这是(相同的)结果:

DispatchQueue asyncAfter演示