Avant de commencer, notez que cela n’a rien à voir avec le traitement en arrière-plan. Il n'y a pas de "calcul" impliqué qui serait de fond.
Seulement UIKit.
view.addItemsA()
view.addItemsB()
view.addItemsC()
Disons sur un iPhone 6s
Cela va arriver:
Mais disons que je veux que cela se produise:
(Notez que "une seconde" n'est qu'un exemple simple pour plus de clarté. Voir la fin de ce post pour un exemple plus détaillé.)
Comment faites-vous cela sur iOS?
Vous pouvez essayer ce qui suit. Cela ne semble pas fonctionner.
view.addItemsA()
view.setNeedsDisplay()
view.layoutIfNeeded()
view.addItemsB()
Vous pouvez essayer ceci:
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() }...
Notez que si la valeur est trop petite - cette approche ne fait rien, et évidemment ne fait rien}. UIKit va simplement continuer à travailler. (Que ferait-il d'autre?) Si la valeur est trop grande, cela ne sert à rien.
Notez que pour le moment (iOS10), si je ne me trompe pas: si vous essayez cette astuce avec l’astuce d’un délai nul, elle fonctionne au mieux de manière erratique. (Comme vous vous en doutez probablement.)
view.addItemsA()
view.setNeedsDisplay()
view.layoutIfNeeded()
RunLoop.current.run(mode: .defaultRunLoopMode, before: Date())
view.addItemsB()
view.setNeedsDisplay()
view.layoutIfNeeded()
Raisonnable. Mais nos tests récents montrent que cela ne semble PAS fonctionner dans de nombreux cas.
(c’est-à-dire que l’UIKit d’Apple est maintenant assez sophistiqué pour gâcher son travail au-delà de ce "truc".)
Pensée: y at-il peut-être un moyen, dans UIKit, d’obtenir un rappel lorsque toutes les vues que vous avez accumulées ont été dessinées? Y a-t-il une autre solution?
Une solution semble être .. mettre les sous-vues dans les contrôleurs, afin que vous obteniez un rappel "didAppear", et les suivre. _ {Cela semble infantile}, mais c'est peut-être le seul motif? Cela fonctionnerait-il vraiment de toute façon? (Un seul problème: je ne vois aucune garantie que didAppear garantisse que toutes les sous-vues ont été dessinées.)
Exemple d'utilisation quotidienne:
• Disons qu'il y a peut-être sept des sections.
• Dites que chacun prend généralement 0.01 à 0.20 pour que UIKit soit construit (en fonction des informations que vous affichez).
• Si vous simplement "laissez le tout aller d'un coup" ce sera souvent OK ou acceptable (temps total, disons 0,05 à 0,15) ... mais ...
• il y aura souvent une pause fastidieuse pour l'utilisateur lorsque le "nouvel écran apparaît". (.1 à .5 ou pire).
• Tandis que si vous faites ce que je vous demande, cela sera toujours affiché à l'écran, morceau par morceau, avec le temps minimal possible pour chaque morceau.
Le serveur de fenêtre a le contrôle final de ce qui apparaît à l'écran. iOS envoie des mises à jour au serveur de fenêtres uniquement lorsque la CATransaction
actuelle est validée. Pour que cela se produise au besoin, iOS enregistre une CFRunLoopObserver
pour l'activité .beforeWaiting
dans la boucle d'exécution du thread principal. Après avoir traité un événement (probablement en appelant votre code), la boucle d'exécution appelle l'observateur avant d'attendre le prochain événement. L'observateur valide la transaction en cours, s'il en existe une. La validation de la transaction inclut l'exécution de la passe de présentation, la passe d'affichage (dans laquelle vos méthodes drawRect
sont appelées) et l'envoi de la disposition et du contenu mis à jour au serveur de fenêtres.
L'appel de layoutIfNeeded
effectue la mise en page, si nécessaire, mais n'invoque pas la passe d'affichage ni n'envoie quoi que ce soit au serveur de fenêtres. Si vous souhaitez que iOS envoie des mises à jour au serveur de fenêtres, vous devez valider la transaction en cours.
Une façon de le faire est d'appeler CATransaction.flush()
. Un cas raisonnable à utiliser CATransaction.flush()
est quand vous voulez mettre une nouvelle CALayer
à l'écran et que vous voulez qu'il ait une animation immédiatement. La nouvelle CALayer
ne sera pas envoyée au serveur de fenêtres jusqu'à ce que la transaction soit validée et vous ne pourrez pas y ajouter d'animation tant qu'elle ne sera pas affichée. Ainsi, vous ajoutez le calque à votre hiérarchie, appelez CATransaction.flush()
, puis ajoutez l'animation au calque.
Vous pouvez utiliser CATransaction.flush
pour obtenir l’effet souhaité. Je ne recommande pas ceci, mais voici le code:
@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)
}
Et voici le résultat:
Le problème avec la solution ci-dessus est qu’il bloque le thread principal en appelant Thread.sleep
. Si votre thread principal ne répond pas aux événements, non seulement l'utilisateur est frustré (parce que votre application ne répond pas à ses contacts), mais, finalement, iOS décide que l'application est bloquée et le tue.
La meilleure solution consiste simplement à planifier l'ajout de chaque vue lorsque vous souhaitez qu'elle apparaisse. Vous prétendez que «ce n’est pas de l’ingénierie», mais vous vous trompez et vos raisons données n’ont aucun sens. iOS met généralement à jour l'écran toutes les 16 millisecondes (à moins que votre application ne prenne plus de temps que cela pour gérer les événements). Tant que le délai souhaité est au moins aussi long, vous pouvez simplement planifier l'exécution d'un bloc après le délai pour ajouter la vue suivante. Si vous souhaitez un délai inférieur à 16 millisecondes, vous ne pouvez généralement pas l'obtenir.
Voici donc le meilleur moyen recommandé d'ajouter les sous-vues:
@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)
}
Et voici le résultat (identique):
3 méthodes qui pourraient fonctionner ci-dessous. La première que je pourrais faire fonctionner si une sous-vue ajoute le contrôleur ainsi que si ce n'est pas directement dans le contrôleur de vue. La seconde est une vue personnalisée :) Il me semble que vous vous demandez quand layoutSubviews est terminé pour moi. Ce processus continu gèle l’affichage en raison des 1000 sous-vues plus séquentiellement. Selon votre situation, vous pouvez ajouter une vue childviewcontroller et publier une notification lorsque viewDidLayoutSubviews () est terminé, mais je ne sais pas si cela correspond à votre cas d'utilisation. J'ai testé avec 1000 sous-marins sur la vue viewcontroller ajoutée et cela a fonctionné. Dans ce cas, un retard de 0 fera exactement ce que vous voulez. Voici un exemple de travail.
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
} }
Ensuite, dans votre contrôleur de vue principal que vous ajoutez des sous-vues, vous pouvez le faire.
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
})
}
}
}
Ou bien il y a ENCORE une manière plus folle et probablement meilleure. Créez votre propre vue qui, dans un sens, conserve son heure et rappelle lorsqu'il est bon de ne pas perdre d'images. Ceci est non poli mais fonctionnerait.
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)
}
}
}
Ou enfin, vous pouvez effectuer le rappel ou la notification dans viewDidAppear, mais il semble également que tout code exécuté sur le rappel aurait besoin d'être encapsulé pour s'exécuter dans le temps imparti à partir du rappel viewDidAppear.
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.0, execute: {
//code})
C'est une sorte de solution. Mais ce n'est pas de l'ingénierie.
En fait, oui. En ajoutant le délai, vous faites exactement ce que vous avez dit que vous vouliez faire: vous permettez à la boucle d'exécution de se terminer et à la présentation d'être effectuée, et vous ré-entrez sur le fil principal dès que cela est fait. C’est en fait une de mes utilisations principales de delay
. (Vous pourriez même être capable d'utiliser une delay
de zéro.)
Utilisez NSNotification
pour obtenir l’effet souhaité.
Commencez par enregistrer un observateur dans la vue principale et créer un gestionnaire d'observateur.
Ensuite, initialisez tous ces objets A, B, C ... dans un thread séparé (en arrière-plan, par exemple), par exemple, avec self performSelectorInBackground
Ensuite - postez une notification à partir des vues secondaires et la dernière - performSelectorOnMainThread
pour ajouter une sous-vue dans l'ordre souhaité avec les délais nécessaires.
Pour répondre aux questions sous forme de commentaires, supposons qu'un UIViewController apparaisse à l'écran. Cet objet - pas un point de discussion et vous pouvez décider où placer le code, en contrôlant l'apparence de la vue. Le code est pour l'objet UIViewController (donc, il est auto). Vue - un objet UIView, considéré comme une vue parent. ViewN - une des sous-vues. Il peut être mis à l'échelle plus tard.
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleNotification:)
name:@"ViewNotification"
object:nil];
Cela a permis à un observateur de communiquer entre les threads . ViewN * V1 = [[ViewN alloc] init]; Ici - les sous-vues peuvent être attribuées - pas encore affichées.
- (void) handleNotification: (id) note {
ViewN * Vx = (ViewN*) [(NSNotification *) note.userInfo objectForKey: @"ViewArrived"];
[self.View performSelectorOnMainThread: @selector(addSubView) withObject: Vx waitUntilDone: FALSE];
}
Ce gestionnaire permet de recevoir des messages et de placer UIView
object dans la vue parent. Cela semble étrange, mais le fait est que vous devez exécuter la méthode addSubview sur le thread principal pour prendre effet. performSelectorOnMainThread
permet de commencer à ajouter une sous-vue sur le thread principal sans bloquer l'exécution de l'application.
Maintenant, nous créons une méthode qui place les sous-vues à l’écran.
-(void) sendToScreen: (id) obj {
NSDictionary * mess = [NSDictionary dictionaryWithObjectsAndKeys: obj, @"ViewArrived",nil];
[[NSNotificationCenter defaultCenter] postNotificationName: @"ViewNotification" object: nil userInfo: mess];
}
Cette méthode publiera une notification à partir de n'importe quel thread, en envoyant un objet sous la forme d'élément NSDictionary appelé ViewArrived.
Et enfin - les vues à ajouter avec un délai de 3 secondes:
-(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];
}
Ce n'est pas la seule solution. Il est également possible de contrôler les sous-vues de la vue parent en comptant la propriété NSArray
subviews
.
Dans tous les cas, vous pouvez exécuter la méthode initViews
chaque fois que vous en avez besoin et même dans un fil d’arrière-plan et cela permet de contrôler l’aspect de la sous-vue, le mécanisme performSelector
permet d’éviter le blocage du fil d’exécution.