在开始之前请注意,这与后台处理无关。没有"计算"涉及到一个人会背景。
仅限UIKit 。
view.addItemsA()
view.addItemsB()
view.addItemsC()
让我们说一下6s的iPhone
这将会发生:
但是,让我说我希望这种情况发生:
(注意"一秒钟"为清晰起见,这只是一个简单的例子。有关更全面的示例,请参阅本文末尾。)
你是如何在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()
合理。 但是我们最近的实际测试表明,在许多情况下这似乎不起作用。
(也就是说,Apple的UIKit现在已经足够精湛,足以让UIKit的工作超出那个"技巧&#34 ;.)
思考:在UIKit中,或许有一种方法可以获得一个回调,基本上,它已经绘制了你已经堆积的所有观点?还有其他解决方案吗?
一个解决方案似乎是......将子视图 放在控制器中 ,这样你就得到了一个" didAppear"回调,并跟踪那些。 这似乎是婴儿,但也许它是唯一的模式?无论如何它真的会起作用吗? (仅仅是一个问题:我没有看到任何保证,确保所有子视图都已被绘制。)
日常用例示例:
•假设有七个部分。
•假设每个人通常需要 0.01到0.20 才能构建UIKit(取决于您显示的信息)。
•如果你只是"让整个事情进行一次打击" 它通常会好或可接受(总时间,比如0.05到0.15)......但是。 ..
•随着"新屏幕出现",用户经常会有一个单调乏味的停顿。 ( .1到.5或更差)。
•如果你按照我要求的方式行事,它将始终平滑到屏幕上,一次一个块,每个块的最小可能时间。
答案 0 :(得分: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)
}
这是结果:
上述解决方案的问题在于它通过调用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)
}
这是(相同的)结果:
答案 1 :(得分:1)
这是一种解决方案。但这不是工程设计。
实际上,是的。通过添加延迟,您正在完全您所说的想要做的事情:您允许完成runloop并执行布局,并在完成后立即重新进入主线程。事实上,这是delay
的主要用途之一。 (您甚至可以使用零delay
。)
答案 2 :(得分:1)
下面可能有效的3种方法。第一个我可以使它工作,如果一个子视图添加控制器,如果它不是直接在视图控制器中。第二个是自定义视图:)似乎你想知道layoutSubviews何时完成对我的视图。由于1000个子视图加上顺序,这个连续过程就是冻结显示。根据您的情况,您可以添加childviewcontroller视图并在viewDidLayoutSubviews()完成时发布通知,但我不知道这是否适合您的用例。我在添加的viewcontroller视图上测试了1000个subs,并且它工作正常。在这种情况下,延迟0将完全符合您的要求。这是一个有效的例子。
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
} }
然后在您的主视图控制器中添加子视图,您可以执行此操作。
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
})
}
}
}
或者有更疯狂的方式,可能更好的方式。创建自己的视图,在某种意义上保持自己的时间,并在不丢帧时回调。这是未经抛光但可行的。
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)
}
}
}
或者最后,您可以在viewDidAppear中执行回调或通知,但似乎还需要包含在回调上执行的任何代码,以便从viewDidAppear回调中及时执行。
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.0, execute: {
//code})
答案 3 :(得分:0)
使用NSNotification
来达到所需的效果。
首先 - 将观察者注册到主视图并创建观察者处理程序。
然后,通过例如self performSelectorInBackground
然后 - 发布子视图和最后一次的通知 - performSelectorOnMainThread
以所需的顺序添加所需的延迟的子视图。
要回答评论中的问题,假设您有一个显示在屏幕上的UIViewController。这个对象 - 不是讨论点,你可以决定在哪里放置代码,控制视图外观。代码用于UIViewController对象(因此,它是self)。视图 - 一些UIView对象,被视为父视图。 ViewN - 子视图之一。它可以稍后缩放。
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleNotification:)
name:@"ViewNotification"
object:nil];
注册了一个观察者 - 需要在线程之间进行通信。 ViewN * V1 = [[ViewN alloc] init]; 这里 - 可以分配子视图 - 尚未显示。
- (void) handleNotification: (id) note {
ViewN * Vx = (ViewN*) [(NSNotification *) note.userInfo objectForKey: @"ViewArrived"];
[self.View performSelectorOnMainThread: @selector(addSubView) withObject: Vx waitUntilDone: FALSE];
}
此处理程序允许接收消息并将UIView
对象放置到父视图。看起来很奇怪,但关键是 - 你需要在主线程上执行addSubview方法才能生效。 performSelectorOnMainThread
允许开始在主线程上添加子视图,而不会阻止应用程序执行。
现在 - 我们制作了一个将子视图放到屏幕上的方法。
-(void) sendToScreen: (id) obj {
NSDictionary * mess = [NSDictionary dictionaryWithObjectsAndKeys: obj, @"ViewArrived",nil];
[[NSNotificationCenter defaultCenter] postNotificationName: @"ViewNotification" object: nil userInfo: mess];
}
此方法将从任何线程发布通知,将对象作为名为ViewArrived的NSDictionary项发送。
最后 - 必须添加3秒延迟的视图:
-(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];
}
这不是唯一的解决方案。也可以通过计算NSArray
subviews
属性来控制父视图的子视图
在任何情况下,您都可以在需要时运行initViews
方法,甚至在后台线程中也可以控制子视图外观,performSelector
机制允许避免执行线程阻塞。