web-dev-qa-db-fra.com

Indicateur d'activité de démarrage et d'arrêt Swift3

J'apprends Swift et travaille maintenant sur ma tâche à la maison. Un de mes boutons appelle une fonction dans laquelle je recharge une vue de collection et cela prend du temps. J'essaie d'implémenter un indicateur d'activité pour montrer le processus ..___ Le problème est que si je lance l'indicateur, charge la vue de collection, puis arrêtez l'indicateur dans l'action du bouton, l'indicateur n'apparaît pas . Quelle serait la bonne façon de mettre en œuvre l'indicateur dans ce cas? Voici le morceau du code:

let activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: UIActivityIndicatorViewStyle.whiteLarge)
@IBOutlet weak var onCIFilterView: UICollectionView!
@IBOutlet var cifilterMenu: UIView!

@IBAction func onCIFiltersButton(_ sender: UIButton) {
    if (sender.isSelected) {
        hideCIFilters()
        sender.isSelected = false
    }
    else {
        activityIndicator.startAnimating()
        showCIFilters()  //the menu with UIView collection
        activityIndicator.stopAnimating()
        sender.isSelected = true
    }
}

func showCIFilters () {
    onCIFilterView.reloadData() // reloading collection view
    view.addSubview (cifilterMenu)
    let bottomConstraint = cifilterMenu.bottomAnchor.constraint(equalTo: mainMenu.topAnchor)
    let leftConstraint = cifilterMenu.leftAnchor.constraint(equalTo: view.leftAnchor)
    let rightConstraint = cifilterMenu.rightAnchor.constraint(equalTo: view.rightAnchor)
    let heightConstraint = cifilterMenu.heightAnchor.constraint(equalToConstant: 80)
    NSLayoutConstraint.activate([bottomConstraint, leftConstraint, rightConstraint, heightConstraint])
    view.layoutIfNeeded()
}

func hideCIFilters () {
    currentImage = myImage.image
    cifilterMenu.removeFromSuperview()

}

override func viewDidLoad() {
    super.viewDidLoad()
    //.....
    activityIndicator.center = CGPoint(x: view.bounds.size.width/2, y: view.bounds.size.height/2)
    activityIndicator.color = UIColor.lightGray
    view.addSubview(activityIndicator)
}
7
Ana

Le problème que vous rencontrez est que les modifications de l'interface utilisateur, telles que le démarrage d'un indicateur d'activité, n'ont lieu que lorsque le code est retourné et que l'application gère la boucle d'événement.

Votre code est synchrone, donc l'indicateur d'activité n'est jamais appelé.

Vous devez commencer à faire tourner l'indicateur d'activité, y retourner, puis faire votre activité fastidieuse après un délai. Vous pouvez utiliser dispatch_after pour cela (ou DispatchQueue.main.asyncAfter dans Swift 3).

Voici un exemple IBAction qui exécute un indicateur d'activité pendant 2 secondes après avoir appuyé sur un bouton:

@IBAction func handleButton(_ sender: UIButton) {
  activityIndicator.startAnimating()
  sender.isEnabled = false
  DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {

    // your time-consuming code here
    sleep(2) //Do NOT use sleep on the main thread in real code!!!
    self.activityIndicator.stopAnimating()
    sender.isEnabled = true
  }

}

Modifier:

Votre code ressemblerait à ceci:

@IBAction func onCIFiltersButton(_ sender: UIButton) {
    if (sender.isSelected) {
        hideCIFilters()
        sender.isSelected = false
    }
    else {
        activityIndicator.startAnimating()
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.0) {
            showCIFilters()  //the menu with UIView collection
            activityIndicator.stopAnimating()
            sender.isSelected = true
        }
    }
}

Edit # 2:

Encore une fois, les modifications de l'interface utilisateur ne se produisent réellement que lorsque votre code est RETOURS et que l'application dessert la boucle d'événement.

Si vous avez un code comme celui-ci:

activityIndicator.startAnimating()
sleep(2)  //This is in place of your time-consuming synchronous code.
activityIndicator.stopAnimating()

Alors qu'est-ce qui se passe est ceci:

Vous mettez un appel en file d'attente pour démarrer l'indicateur d'activité au prochain retour de votre code. (Donc, cela n'arrive pas encore.)

Vous bloquez le thread principal avec une tâche fastidieuse. Pendant ce temps, votre indicateur d'activité n'a pas encore commencé à tourner.

Enfin, votre tâche prend du temps. Votre code appelle maintenant activityIndicator.stopAnimating(). Enfin, votre code revient, votre application traite la boucle d'événements et les appels à démarrer puis à arrêter immédiatement l'indicateur d'activité sont appelés. En conséquence, l'indicateur d'activité ne tourne pas réellement.

Ce code le fait différemment:

//Queue up a call to start the activity indicator
activityIndicator.startAnimating()

//Queue up the code in the braces to be called after a delay.
//This call returns immediately, before the code in the braces is run.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
    showCIFilters()  //the menu with UIView collection
    activityIndicator.stopAnimating()
    sender.isSelected = true
}

Étant donné que la fermeture transmise à l'appel de DispatchQueue.main.asyncAfter n'est pas exécutée immédiatement, cet appel est renvoyé immédiatement. 

L'application retourne au service de la boucle d'événements et l'indicateur d'activité commence à tourner. Peu de temps après, Grand Central Dispatch appelle le bloc de code que vous lui avez transmis, qui commence à exécuter votre opération fastidieuse (sur le thread principal), puis lance un appel pour arrêter l'indicateur d'activité une fois la tâche fastidieuse terminée. .

11
Duncan C

Voici une chose que vous pouvez essayer: - J'ai créé une vue de chargement personnalisée et l'ai affichée et masquée lors de clics de bouton. Vous pouvez afficher et masquer en fonction de vos besoins particuliers. Pour une démonstration complète, vous pouvez voir ce dépôt sur github - NKLoadingView dans Swift

import UIKit

class NKLoadingView: UIView {

let width: CGFloat = 150
let height: CGFloat = 60
let cornerRadius: CGFloat = 15
let spacing: CGFloat = 20
var label = UILabel()
var fontSize: CGFloat = 20
var isVisible: Bool = false


override init(frame: CGRect) {
    let window = UIApplication.shared.keyWindow!
    let viewFrame = CGRect(x: 50, y: window.frame.height / 2 - height / 2, width: window.frame.width - 2 * 50, height: height)
    super.init(frame: viewFrame)
    setupView()
}

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
}

func setupView(){
    self.layer.cornerRadius = cornerRadius
    self.layer.backgroundColor = UIColor.black.withAlphaComponent(0.65).cgColor
    let spinner = UIActivityIndicatorView(activityIndicatorStyle: .white)
    spinner.startAnimating()
    spinner.frame = CGRect(x: CGFloat(20), y: CGFloat(10), width: 40, height: 40)

    label = UILabel(frame: CGRect(x: spinner.frame.Origin.x + spinner.frame.width + spacing, y:  spinner.frame.Origin.y, width: self.frame.width - spacing - spinner.frame.width - 20, height: spinner.frame.height))
    label.text  = "Please Wait..."
    label.textColor = UIColor.white
    label.font = UIFont.systemFont(ofSize: fontSize)

    self.addSubview(spinner)
    self.addSubview(label)
}

func stopAnimating(){
    isVisible = !isVisible
    self.removeFromSuperview()
}

func startAnimating(message: String){
    label.text = message
    isVisible = !isVisible
    UIApplication.shared.keyWindow!.addSubview(self);

 }
}

//View Controller code
class ViewController: UIViewController {

var loading: NKLoadingView?
override func viewDidLoad() {
    super.viewDidLoad()

    loading = NKLoadingView()
}

override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()

}

@IBAction func startEndAnimation(_ sender: Any) {
    if !loading!.isVisible {
        loading?.startAnimating(message: "Please Wait....")
        (sender as! UIButton).setTitle("Stop Animation", for: .normal)
    }else{
        loading?.stopAnimating()
        (sender as! UIButton).setTitle("Start Animation", for: .normal)
    }
}

}
1
Nikesh Jha

Duncan C a raison, vous devriez effectuer votre activité fastidieuse de manière asynchrone. Cependant, la meilleure façon de l'implémenter est d'ajouter un complétionHandler à la fonction onCIFilterView.reloadData(). De cette façon, vous pouvez arrêter l'indicateur d'activité au moment où le rechargement des données est terminé. Il existe un bon tutoriel sur completionHandlers ici: https://grokswift.com/completion-handlers-in-Swift/

De plus, je pense qu'il y a quelque chose d'autre qui se passe ici aussi. Si vous ne le voyez pas, c'est que CIFilterView se trouve au-dessus de celui-ci dans la pile de vues de l'interface utilisateur. Vous devrez le signaler. Pour ce faire, vous devez appeler quelque chose comme:

view.bringSubview(toFront: activityIndicator)

ou

view.superclass?.bringSubview(toFront: activityIndicator)
0
matt.writes.code