web-dev-qa-db-fra.com

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

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

CHAQUE de ceux-ci prend une seconde pour que UIKit construise.

Cela va arriver:

 one big step

ILS APPARAISSENT TOUT AT UNE FOIS. Pour répéter, l’écran s’accroche simplement pendant 3 secondes pendant que UIKit fait un travail énorme. Ensuite, ils apparaissent tous à la fois.

Mais disons que je veux que cela se produise:

 enter image description here

ILS APPARAISSENT PROGRESSIVEMENT. L'écran s'accroche simplement pendant 1 seconde pendant que UIKit en construit un. Il semble. Il se bloque à nouveau pendant qu'il construit le suivant. Il semble. Etc.

(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.)

Faites le tour de la boucle ...

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


Au cas où ce n'est pas encore clair ...

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.

27
Fattie

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:

 CATransaction.flush demo

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

 DispatchQueue asyncAfter demo

10
rob mayoff

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. 

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

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

1
matt

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 UIViewobject 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é NSArraysubviews.
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.

0
ETech