29 votes

Comment, simplement, attendre une mise en page dans iOS?

Avant de commencer remarque que cela n'a rien à faire avec le traitement de fond. Il n'y a pas de "calcul" impliquait que l'on pourrait arrière-plan.

Seulement UIKit.

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

Disons que sur un iPhone 6s

CHACUN d'eux prend une seconde pour UIKit à construire.

Ce qui va se passer:

one big step

ILS APPARAISSENT TOUT À LA FOIS. Pour répéter, l'écran se bloque tout simplement pendant 3 secondes alors que UIKit t d'une énorme quantité de travail. Ensuite, ils semblent tous à la fois.

Mais disons que je veux que cela arrive:

enter image description here

ILS APPARAISSENT PROGRESSIVEMENT. L'écran se bloque tout simplement pendant 1 seconde UIKit construit une. Il s'affiche. Il se bloque à nouveau lors de la génération de la suivante. Il s'affiche. Et ainsi de suite.

(Remarque "une seconde" n'est qu'un simple exemple pour plus de clarté. Voir la fin de ce post pour une meilleure exemple.)

Comment voulez-vous faire dans iOS?

Vous pouvez essayer ce qui suit. Il 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 simplement, et bien évidemment, ne fait rien. UIKit va tout simplement continuer à travailler. (Quoi d'autre aurait-il?). Si la valeur est trop grande, il ne sert à rien.

  • Notez qu'actuellement (iOS10), si je ne me trompe pas: si vous essayez cette astuce avec le truc d'un délai de zéro, il fonctionne de manière erratique, au mieux. (Comme vous le feriez probablement s'attendre.)

Voyage de l'exécution de la boucle...

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

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

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

Raisonnable. Mais notre récente de la vie réelle de test indique que cela semble ne PAS fonctionner dans de nombreux cas.

(c'est à dire, Apple UIKit est maintenant suffisamment sophistiqué pour frottis UIKit travailler au-delà de cette "astuce".)

Pensée: est-il peut-être un moyen, dans UIKit, pour obtenir un rappel lorsqu'il a, pour l'essentiel, établi tous les points de vue que vous avez empilés? Est-il une autre solution?

Une solution semble être .. mettre les sous-vues dans les contrôleurs, de sorte que vous obtenez un "didAppear" de rappel et de suivi de ces. Qui semble infantile, mais c'est peut-être le seul modèle? Serait-il vraiment, de toute façon? (Simplement une question: je ne vois pas de garantie que didAppear s'assure que tous les sous-vues ont été tirées.)


Dans le cas où ce n'est toujours pas clair...

Exemple de l'utilisation quotidienne de cas:

• Dire qu'il y a peut-être sept sections.

• Dire que chacun prend généralement de 0,01 à 0,20 pour UIKit de construire (selon quelles sont les informations dont vous faites preuve).

• Si vous venez de "laisser tout aller dans un whack" il sera souvent OK " ou "acceptable" (durée totale, dire de 0,05 à 0,15) ... mais ...

• il y aura souvent une tâche fastidieuse pause pour l'utilisateur en tant que "nouvel écran apparaît". (.1 pour .5 ou pire).

• Alors que si vous faites ce que je demande, il restera toujours en douceur sur l'écran, un morceau à la fois, avec le minimum de temps possible pour chaque morceau.

10voto

rob mayoff Points 124153

La fenêtre du serveur qui a le dernier contrôle de ce qui apparaît sur l'écran. iOS envoie uniquement les mises à jour de la fenêtre du serveur lorsque le courant CATransaction s'est engagé. Pour ce faire, lorsque cela est nécessaire, iOS enregistre un CFRunLoopObserver de la .beforeWaiting de l'activité sur le thread principal de l'exécution de la boucle. Après la manipulation d'un événement (probablement par l'appelant dans votre code), l'exécution de la boucle appels de l'observateur, avant qu'il attend pour le prochain événement à arriver. L'observateur valide la transaction en cours, s'il y en a un. La validation de la transaction comprend l'exécution de la passe de mise en page, l'affichage passe (dans lequel votre drawRect méthodes sont appelées), et l'envoi de la mise à jour de mise en page et le contenu de la fenêtre du serveur.

Appelant layoutIfNeeded effectue la mise en page, si nécessaire, mais ne pas invoquer l'affichage passe ou envoyer quoi que ce soit à la fenêtre du serveur. Si vous voulez iOS pour envoyer des mises à jour de la fenêtre du serveur, vous devez les valider la transaction en cours.

Une façon de le faire est d'appeler CATransaction.flush(). Un nombre raisonnable de cas d'utilisation CATransaction.flush() , c'est quand vous voulez mettre un nouveau CALayer sur l'écran et vous voulez qu'elle ait une animation immédiatement. Le nouveau CALayer ne sont pas envoyés à la fenêtre du serveur jusqu'à ce que la transaction est validée, et vous ne pouvez pas ajouter des animations jusqu'à ce qu'il y a à l'écran. Donc, vous ajoutez la couche sur la couche de hiérarchie, appelez - CATransaction.flush(), puis ajouter de l'animation à la couche.

Vous pouvez utiliser CATransaction.flush pour obtenir l'effet que vous voulez. Je ne le recommande pas, 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:

CATransaction.flush demo

Le problème avec la solution ci-dessus est que, il bloque le thread principal en appelant Thread.sleep. Si votre thread principal ne répond pas à des événements, non seulement l'utilisateur frustré (parce que votre application n'est pas en réponse à ses touches), mais finalement, iOS va décider que l'application est suspendue et le tuer.

La meilleure façon est simplement pour planifier l'ajout de chaque point de vue lorsque vous souhaitez qu'il apparaisse. Vous prétendez "ce n'est pas le génie", mais vous avez tort, et vos raisons n'ont pas de sens. iOS généralement les mises à jour de l'écran à chaque 16⅔ millisecondes (à moins que votre application prend plus de temps que pour gérer les événements). Tant que le délai que vous voulez est au moins aussi longtemps, vous pouvez simplement prendre un bloc à exécuter après le délai d'ajouter la vue suivante. Si vous voulez un retard de moins de 16⅔ millisecondes, vous ne pouvez pas, en général, ont.

Voici donc le mieux, 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 (identiques) résultat:

DispatchQueue asyncAfter demo

1voto

matt Points 60113

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 autorisez la boucle d'exécution à se terminer et la mise en page à effectuer, et vous rentrez sur le thread principal dès que cela est fait. En fait, c'est l'une de mes principales utilisations de delay . (Vous pourriez même être en mesure d'utiliser un delay de zéro.)

1voto

agibson007 Points 2326

3 méthodes qui pourraient travailler ci-dessous. La première fois que je pouvais le faire fonctionner si une sous-vue est l'ajout du contrôleur ainsi, si c'est pas directement dans le point de vue du contrôleur.Le deuxième est un affichage personnalisé :) Il semble que vous vous demandez quand layoutSubviews est terminé sur le point de vue à moi. Ce processus continu est ce qui est le gel de l'affichage en raison de l'1000 sous-vues plus de manière séquentielle. Selon votre situation, vous pouvez ajouter un childviewcontroller d'afficher et de publier une notification lorsque viewDidLayoutSubviews() est terminé, mais je ne sais pas si cela correspond à votre cas d'utilisation. J'ai testé avec 1000 subs sur le viewcontroller vue d'être ajoutée et cela a fonctionné. Dans ce cas, un retard de 0 va faire 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
} }

Puis dans votre Vue principale du Contrôleur que vous ajoutez des sous-vues vous pouvez faire cela.

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 il est ENCORE plus fou, et même mieux. Créez votre propre point de vue que dans un sens, conserve son propre temps et des appels en arrière quand il est bon de ne pas laisser tomber les cadres. C'est mat, mais pourrait fonctionner.

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

Ou Enfin,vous pouvez faire le rappel ou de notification à l'viewDidAppear mais il semble également que le code exécuté sur le rappel aurait besoin d'être enveloppé par l'exécution en temps opportun de la viewDidAppear de rappel.

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

0voto

GOST Points 427

Utiliser NSNotification pour atteindre effet nécessaire.

Premier registre un observateur à la vue principale et de créer observateur gestionnaire.

Puis, d'initialiser tous les A,B,C... des objets dans le thread séparé (arrière-plan, par exemple), par exemple - self performSelectorInBackground

Puis - poste de la notification de sous-vues et le dernier - performSelectorOnMainThread d'ajouter des sous-vue dans l'ordre souhaité avec les délais nécessaires.
Pour répondre aux questions dans les commentaires, disons que vous avez un UIViewController qui était affiché sur l'écran. Cet objet, pas un point de discussion et vous pouvez décider de l'endroit où placer le code, le contrôle de l'apparence. Le code est pour le UIViewController objet (donc, c'est une auto). Vue - certains UIView objet, considéré comme un parent de la vue. ViewN - l'un des sous-vues. Il peut être adapté plus tard.

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

Cette inscrit un observateur nécessaires à la communication entre les threads. ViewN * V1 = [[ViewN alloc] init]; Ici - sous-vues peuvent être attribuées - pas montré encore.

- (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 place UIViewobjet de la vue parent. Semble étrange, mais le point est que vous devez exécuter addSubview méthode sur le thread principal pour prendre effet. performSelectorOnMainThread permet de commencer à ajouter des sous-vue sur le thread principal, sans bloquer l'exécution de l'application.

Maintenant, nous mettons en place une méthode qui permettra de placer 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 post à partir de n'importe quel thread, l'envoi d'un objet comme NSDictionary élément nommé ViewArrived.
Et enfin des points de vue qui doivent être ajoutés avec 3 secondes de retard:

-(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 l' NSArray subviews de la propriété.
Dans tous les cas, vous pouvez exécuter initViews méthode à chaque fois que vous avez besoin et même dans le thread d'arrière-plan et il permet le contrôle de la sous-vue apparence, performSelector mécanisme permet d'éviter l'exécution de blocage du thread.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X