Externaliser des méthodes nécessitant des mises à jour de contenu régulier

Bonjour tout le monde,

J’essaye de “découper” mon application en séparant bien le fonctionnement (dans plusieurs classes, etc), mais je suis face à un cas que je ne sais pas comment résoudre.

Lorsque j’ai une fonction qui nécessite régulièrement des mises à jours de contenu, comment est-ce que je peux faire pour la mettre dans un autre fichier que celui qui lui donne régulièrement les informations?

Avec un exemple, ça sera plus parlant je pense:
Imaginons que je suis dans un fichier nommé “JeuViewController”, ce viewController est celui qui gère le jeu auquel le joueur joue (le jeu n’a pas d’importance).
Dans ce même fichier, j’ai une méthode “verificationScore”, qui, toutes les 10 secondes (via Timer par exemple) reçoit le score de la partie en cours (donc une donnée contenue dans “JeuViewController”), vérifie si c’est le meilleur score ou mieux, et ensuite m’affiche en console si c’est le meilleur score ou non.

Pour faire tout ça, tout va bien, ça fonctionne.
Maintenant, j’aimerai pouvoir externaliser ma fonction “verificationScore” et la mettre dans un autre fichier.

Mais comme cette fonction nécessite une mise à jour de donnée de la part de JeuViewController, je ne vois pas comment je peux l’externaliser… Puisque si je la mets dans un autre fichier, je n’arrive plus à lui faire parvenir les données qu’elle a besoin…

Le soucis étant que j’ai cette méthode a plusieurs endroits dans mon code, alors, j’aimerai éviter de le dupliquer…

J’espère avoir été plus ou moins clair dans mes explications…

Merci,

Alexandre

Pourquoi ne pas créer une classe spécifique pour contrôler régulièrement la valeur du score ?

Exemple (testé en Swift 4 avec Xcode 9 bêta 6) :

import Foundation

class SurveillanceRecord {
var record = 0
var scoreActuel = 0
private var timer : Timer?

init() {
    timer = Timer.scheduledTimer(
        withTimeInterval: 10.0,
        repeats: true,
        block: { _ in
            print ("Contrôle du record toutes les 10 secondes")
            if self.scoreActuel > self.record {
                self.record = self.scoreActuel
                print ("Nouveau record : ", self.record)
            }}  )
}

deinit {
    timer?.invalidate()
    timer = nil
}

}

la classe possède 2 variables : le record du jeu et le score du jeu. Toutes les 10 secondes son timer interne lui demande de comparer les deux valeurs. La surveillance commence immédiatement après la création d’un SurveillanceRecord. On peut améliorer cela en ajoutant des méthodes pour activer ou désactiver la surveillance par code.

Le timer doit être détruit à la destruction de l’objet de surveillance. Normalement c’est fait automatiquement, mais j’ai préféré utiliser deinit() pour en être certain.

Utilisation dans un ViewController :

import UIKit

class ViewController: UIViewController {
    
    // Le Score
    var score = 0
    // L'objet surveillant le score
    let surveillanceRecord = SurveillanceRecord()

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Initialisation des scores
        surveillanceRecord.scoreActuel = 0
        surveillanceRecord.record = 0
    }

    // Un bouton permet de modifier le score de 1
    @IBAction func actionModifierRecord(_ sender: Any) {
        // Gestion du score
        score += 1
        // Notification de la modification au système de surveillance
        surveillanceRecord.scoreActuel = score
    }
    
}

Le ViewController crée l’objet de surveillance, et l’informe de toutes les modifications du score. Pour tester le système, j’ai simulé l’évolution du jeu avec un bouton incrémentant le score à chaque action.

Affichage

Contrôle du record toutes les 10 secondes
Contrôle du record toutes les 10 secondes
Nouveau record :  5
Contrôle du record toutes les 10 secondes
Nouveau record :  7

L’utilisation du Timer ne me parait pas justifié pour contrôler le score du jeu, mais j’ai supposé que c’est juste un exemple simplifié pour illustrer un concept, alors j’ai codé ça selon ta demande.

Moi j’aurais juste utilisé le didSet de la classe de surveillance pour contrôler en temps réel l’évolution du score, sans avoir de Timer.

class VerificationRecord {
    var record = 0
    var scoreActuel = 0 {
        didSet {
            if scoreActuel > record {
                record = scoreActuel
                print ("Nouveau record : ", record)
            }
        }
    }
}
1 « J'aime »

Pouah :o
Merci @Draken pour cette précieuse aide! :smile:

Effectivement, cela sera bien plus propre comme ça!
Je n’avais pas pensé que je pouvais initialiser le timer directement dans la classe, et ainsi n’avoir plus qu’à créer la classe pour que celui-ci se mette en route.

Pour l’utilisation d’un timer, oui, dans l’exemple que j’ai donné, j’aurai pu m’en passé, mais c’est pour essayer de faire un exemple simple par rapport à ce que je veux faire, et où j’aurai besoin d’un timer.

Par contre, comment est-ce que je peux faire si maintenant, dans mon ViewController, je souhaite être informé de l’évolution du timer?
Par exemple, dans le cas où le timer est utilisé comme un décompte, et que lorsqu’il arrive à 0, je puisse en être informé dans mon ViewController? (Pour détruire cet objet par exemple)

Encore merci! :slight_smile:

La classe externalisé peut communiquer avec le ViewController en passant par une closure. C’est un petit morceau de code que l’on peut transmettre comme une variable. Par exemple j’ai codé une classe Compteur décrémentant une variable, avec un délai d’attente. A chaque changement de la valeur, elle exécute une closure (que j’ai appelé callBack).

class Compteur {
    var valeur = 0
    private var timer : Timer?

    init(valeurDebut:Int, delai:TimeInterval, callBack: @escaping ()->Void) {
        valeur = valeurDebut
        timer = Timer.scheduledTimer(withTimeInterval: delai,
                                    repeats: true,
                                    block: {_ in
                                        // Décrémentation du compteur
                                        self.valeur -= 1
                                        // Exécution de la closure
                                        callBack()})
    }
    
    deinit {
        timer?.invalidate()
        timer = nil
    }
}

Cela s’utilise comme ça :

class ViewController: UIViewController {
    
    var compteur : Compteur?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        compteur = Compteur(
                 valeurDebut:10,
                 delai: 1.0,
                 callBack: { self.gererCompteur() })
    }
    
    func gererCompteur() {
        // Lecture de la valeur
        if let valeur = compteur?.valeur {
            // Affichage valeur
            print ("Valeur compteur : ", valeur)
            // Destruction du compteur à 0
            if valeur == 0 {
                compteur = nil
                print ("Destruction du compteur")
            }
        }
    }   
}

A chaque changement de valeur du compteur, la closure appelle la fonction gererCompteur() du ViewController. C’est similaire à une IBAction de Storyboard, un événement survenant au sien d’une classe provoquant l’exécution d’une fonction du ViewController.

Tu peux définir plusieurs closures dans la fonction, pour être averti de différents événements.

Le @escaping devant la définition de la closure indique qu’elle s’exécute à l’extérieur de la classe (logique puisque c’est un appel à une fonction du ViewController). Xcode ajoute automatiquement ce paramètre s’il est oublié à la saisie.

Un grand merci @Draken pour ces explications! :slight_smile:

D’ailleurs, je suppose quand l’appel de la fonction callback dans la classe compteur peut-être appelée dans une condition (pour n’appeler cette fonction uniquement si c’est la fin du décompte par exemple.

Ce qui rend tout ça vraiment pratique pour externaliser du code, et que cela reste plus propre que de tout laisser dans le même fichier!

Chouette, je vais donc pouvoir utiliser les ViewController uniquement pour l’interaction avec l’interface, et externaliser le reste dans des classes.

Merci, :slight_smile:

Tiens, dernière petite question: est-il possible de passer des arguments de la classe vers le viewController via une closure?

Par exemple dans la classe:

var secondes:Int = 10
callBack: @escaping (secondes:Int)->Void

mais ensuite, dans le viewController?

callBack: { self.gererCompteur(secondes: ?? ) } // Comment faire pour récupérer l'argument?

func gererCompteur(secondes:Int) {
    // ...
} 

Merci :slight_smile:

J’ai cherché à faire ça ce matin, sans arriver à trouver la bonne syntaxe. Je vais m’y replonger ce soir. Si je ne trouve pas, je demanderais sur Cocoacafe.fr. A moins que Maxime ne passe dans le coin ?

J’ai trouvé, je mets la syntaxe dans 2 minutes :slight_smile:

Dans la classe (Decompte.swift):

init(callBack: @escaping (Int)->Void) {
    callBack(10)
}

et ensuite dans le viewController:

var decompte = Decompte(callback: { (Int) in
        print("\(Int)") // Affiche le nombre Int passé en paramètre depuis la classe Decompte.swift (dans ce cas-ci: 10)
})

Cela fonctionne chez moi :slight_smile:

J’ai aussi trouvé de mon coté. Il est préférable de donner un nom au paramètre de la closure pour accéder à sa valeur. Tu devrais écrire :

var décompte = Decompte(callback: { (valeur:Int) in print(« Valeur reçue : « ,valeur) }

Petit exemple avec une fonction de notification dans un ViewController :

class MonObjet {

    init(closure: @escaping (Int)->Void) {
        // Traitements divers
        // ....
        closure(23)
    }
}



class ViewController: UIViewController {
        
        var monObjet:MonObjet?

        override func viewDidLoad() {
            super.viewDidLoad()
            
            monObjet = MonObjet(closure: { (valeur:Int) in self.notificationCompteur(valeur) })                
        }
        
        // Traitement notification 
        func notificationCompteur(_ valeur:Int) {
            print ("Valeur : ", valeur)
        }  
    }

Oui, dans le viewController, il vaut mieux donner une valeur effectivement, le “Int” correspond en fait au nom que l’on souhaite donner à la variable.

Par contre, encore une petite question: dans ton dernier exemple:

init(closure: @escaping (Int)->Void) {
    // Traitements divers
    // ....
    closure(23)
}

si à la place d’avoir:

closure(23)

on a quelque chose comme:

init(updateUI: @escaping (Int, Double, Double) -> Void) {

    // ... traitement

    monTimer = Timer.scheduledTimer(
        withTimeInterval: 1,
        repeats: true,
        block: { _ in
            updateUI(self.param1, self.param2)
    })

}

et que dans notre code, on a besoin d’appeler plusieurs fois l’initialisation du timer, comment peut-on faire? puisque le timer a besoin de:

updateUI: @escaping (Int, Double, Double) -> Void

donc comment faire pour pouvoir mettre l’initialisation du timer dans une fonction à part? J’imagine que je dois plutôt définir une closure pour l’initialisation du timer, et que je dois l’appeler depuis la fonction init() ?

Cela devient compliqué ton système. Il me parait plus simple de détruire l’objet après son utilisation en mettant sa valeur à nil. Et d’en recréer une avec les nouveaux paramètres.

Sinon, tu peux utiliser l’ancienne version de Timer qui n’utilise pas un block: pour fonctionner, mais appelle une fonction de la classe. L’appel de la closure n’étant plus liée à l’initialisation du Timer, tu peux le réinitialiser sans problème.

https://developer.apple.com/documentation/foundation/timer/1412416-scheduledtimer

Je vais regarder à ça, merci :slight_smile:

Maintenant que j’y pense, une closure s’utilise exactement comme une variable. Tu peux en faire une copie à l’initialisation, pour s’en servir plus tard, comme la réinitialisation d’un Timer.

class MonObjet {
    var _copieClosure : (Int)->Void?
    
    init(closure: @escaping (Int)->Void) {
        _copieClosure = closure
        // Traitements divers
        // ....
        closure(23)
    }
}

Hello,

Pour ce genre de fonctionnement une seul chose me vient a l’esprit, une class en mode standalone.