Problème de design pattern, comment le controller sait quelle vue représente quel modelObject ?

2»

Réponses

  • Bon, j'ai fait un truc mais la syntaxe est trop hard pour quelqu'un ne connaissant pas le swift. Je vais le refaire différemment et je poste ça demain, avec des explications.

  • DrakenDraken Membre
    25 avril modifié #33

    @Draken, est-ce que j'oserai te demander un petit retour s'il te plaît ? Navré de te solliciter, si tu n'as pas le temps je comprends parfaitement !

    En fait, j'ai écrit quelque chose de très proche de ton code.

    L'idée est d'utiliser un identifiant unique pour lier les objets du Modèle et les éléments graphiques.

    Pour commencer j'ai créé un générateur d'identifiant unique. Ta technique d'utiliser un générateur aléatoire n'est pas propre .. Il y a une (toute petite) chance de générer deux fois le même identifiant avec ton système.

    class GenerateurIdentifiantUnique {
        private var compteur = 0
    
        func creerIdentifiant() -> Int {
            let identifiant = compteur
            compteur += 1
            return identifiant
        }
    
    }
    

    Avec un compteur on élimine le risque de répétition. Ce n'est pas parfait, il faudrais ajouter un mécanisme de recyclage des identifiants.

    L'objet Modèle contient une information et un identifiant :

    class UnTruc {
        var informations = "Nom du Truc"
        var identifiant:Int?
    }
    

    Sa représentation graphique est juste une vue et son identifiant. SANS LIEN DIRECT AVEC L'OBJET MODELE, à la différence de ton propre code !

    class UnTrucGraphique : UIView {
        var identifiant:Int?
    }
    

    Le Modèle est un simple tableau

        // Le Modèle
        private var listeDesTrucs = [UnTruc]()
    

    Pour récupérer la position d'un objet dans le tableau à partir de son identifiant, j'ai codé une petite fonction :

    // Recherche de la position d'un objet dans le Modèle
    // à partir de son identifiant
    func chercherIndexObjetTruc(identifiant:Int) -> Int? {
        return listeDesTrucs.index(where: {$0.identifiant == identifiant})
    }
    

    Pour placer un Truc sur l'écran, j'utilise cette méthode :

    func creerUnTruc(position:CGPoint, information:String) {
            // Création identifiant
            let identifiant = generateurIdentifiant.creerIdentifiant()
            // Ecriture dans le Modèle
            let truc = UnTruc()
            truc.informations = information
            truc.identifiant  = identifiant
            // Stockage truc dans le Modèle
            listeDesTrucs.append(truc)
    
            // Représentation graphique
            let trucGraphique = UnTrucGraphique()
            trucGraphique.identifiant = identifiant
            trucGraphique.frame.size = CGSize(width: 70.0, height: 100.0)
            trucGraphique.center = position
            trucGraphique.backgroundColor = UIColor.cyan
            // Placement du truc graphique sur l'écran
            self.view.addSubview(trucGraphique)
    
            // Ajout de la gesture
            let gesture = UITapGestureRecognizer(target: self, action: #selector(self.gestureToucher(_:)))
            trucGraphique.addGestureRecognizer(gesture)
            trucGraphique.isUserInteractionEnabled = true
    
        }
    

    Elle fabrique un objet UnTruc, lui associe un identifiant unique et une information, puis stocke le tout dans le Modèle. Ensuite création d'un objet UnTrucGraphique (UIView en fait) avec le même identifiant. Une gesture est associée pour réagir quand l'utilisateur sélectionne le truc sur l'écran.

        // Traitement gesture
        @objc func gestureToucher(_ sender:UITapGestureRecognizer) {
    
            // Lecture de l'identifiant
            guard let trucGraphique = sender.view as? UnTrucGraphique,
                  let identifiant   = trucGraphique.identifiant
            else { return }
    
            // Recherche de l'objet Modèle à partir de l'identifiant
            if let index = chercherIndexObjetTruc(identifiant: identifiant) {
                // Je récupére l'information
                let monInfo = listeDesTrucs[index].informations
                // Je fais quelque chose avec l'information
                print ()
                print ("Identifiant du Truc : ", identifiant)
                print ("Information : ", monInfo)
            }
        }
    

    Quand la gesture se déclenche, l'application récupère une référence sur l'objet graphique (sender.view), fait un casting en UnObjetGraphique pour récupérer l'identifiant.

    Elle recherche ensuite l'index de l'objet dans le Modèle et affiche les informations. Comme tu peux le constater, il n'y a aucun lien direct entre le Modèle et la Vue. Tout passe par l'identifiant !

    Le code du ViewController :

    import UIKit

    class ViewController: UIViewController {

    private var generateurIdentifiant = GenerateurIdentifiantUnique()
    // Le Modèle
    private var listeDesTrucs = [UnTruc]()
    
    // Initialisation
    override func viewDidLoad() {
        super.viewDidLoad()
        creerLesTrucs()
    }
    
    func creerLesTrucs() {
        creerUnTruc(position: CGPoint(x: 60.0, y: 120.0),
                    information: "Moi j'aime les frites")
        creerUnTruc(position: CGPoint(x: 180.0, y: 120.0),
                    information: "Vive les Sushis !")
        creerUnTruc(position: CGPoint(x: 290, y: 150),
                    information: "Les algues c'est bon !")
    }
    
    func creerUnTruc(position:CGPoint, information:String) {
      ...        
    }
    
    // Recherche de la position d'un objet dans le Modèle
    // à partir de son identifiant
    func chercherIndexObjetTruc(identifiant:Int) -> Int? {
        return listeDesTrucs.index(where: {$0.identifiant == identifiant})
    }
    
    // Traitement gesture
    @objc func gestureToucher(_ sender:UITapGestureRecognizer) {
        ....
    }
    

    }

    C'est juste un premier jet. Il manque pas mal de choses, mais cela te donne déjà une idée.

    Et ça marche, la preuve en image :

  • FKDEVFKDEV Membre

    Le modèle ne doit pas avoir de référence explicite vers la vue. Je pense qu'on sera tous d'accord là-dessus.

    Quand ton modèle change, le reste du logiciel peut-être prévenu par les moyens suivants:
    -Notification
    -KVO (si tes objets dérivent de NSObject)
    -un pattern observer "maison".

    Dans ton cas, j'utiliserai les KVO pour lier chaque objet à la vue qui l'affiche. Mais après cela dépend des propriétés du modèle que tu observes.

  • Bonjour à tous,

    @Joanna Carter:
    Merci, je suis toujours preneur pour une information ou un commentaire :)

    @FKDEV :
    Merci, oui je pense en effet qu'on est tous d'accord là dessus (en théorie, en pratique j'ai l'impression qu'il me manque une brique).

    @Draken :
    Merci pour cet exemple ! N'aies pas peur d'y aller un peu dur avec Swift, je suis à fond dedans en ce moment et j'en ai déjà fais un peu par le passé et ça ne peut pas faire de mal.
    Ton code est parfaitement clair et compréhensible, donc encore une fois un grand merci d'avoir pris le temps d'écrire tout ça !

    Je suis très content car on touche ensemble le coeur d'un des problèmes. En effet dans la méthode chercherIndexObjetTruc(identifiant:Int) -> Int, tu fais une boucle sur potentiellement un très grand nombre d'objets.
    Je suppose que quand tu vas vouloir supprimer les objets suivant dans la colonne grâce au UIMenuController tu vas écrire quelque chose comme ceci pour retrouver la vue représentant le modèle:

    chercherIndexTrucGraphique(identifiant: Int) -> Int? {
        let arrayDeViews : [UnTrucGraphique] ... // le tableau qui contient les vues, ça peut être une référence dans le controller ou directement dans `view.subviews` pour l'exemple si on prend soin de vérifier le type des vues.
        return arrayDeViews.index(where: {$0.identifiant == identifiant});
    }
    

    On se rend compte qu'ajouter un Int ou une référence sur le modèle à la vue importe peu, au final on va devoir boucler sur toutes les vues pour savoir s'ils ont l'identifiant correct.

    La différence entre avoir une référence du modèle sur la vue et un Int comme identifiant est qu'avec la référence, quand on change une vue on peut directement retrouver l'objet du modèle associé, par contre quand on change un objet du modèle il faut boucler sur les vues pour savoir si la vue fait référence à l'objet du modèle qu'on vient de modifier.
    Avec des Int en identifiant il faut toujours boucler, qu'on change la vue ou le modèle, il faut regarder dans l'autre tableau quel objet/vue fait référence à ce qu'on veut, aucun moyen de le savoir autrement.

    Comme dit, je procédais de manière similaire mais j'ai le sentiment que cette boucle n'est pas correcte et pas optimale, qu'il n'est pas possible qu'il n'existe pas de moyen moins couteux de lier les deux. J'espère avoir été précis sur pourquoi cette boucle me dérange car je l'ai déjà abordée maladroitement plusieurs fois dans mes réponses précédentes et ça ne semblait pas être pris en compte, maintenant qu'on voit ce code, c'est plus clair je pense.

  • DrakenDraken Membre
    26 avril modifié #36

    Tu te focalise trop sur cette histoire de boucle. C'est rapide une boucle en Swift, surtout en utilisant les fonctions de recherche optimisée du framework. Il ne faut jamais chercher à optimiser avant la phase de codage, mais toujours à privilégier les choses simples. Ensuite on regarde ce que cela donne en situation réelle et quelle partie du code il faut vraiment optimiser.

    Ensuite, tu dois te rendre compte que tu n'aurais jamais plus de 200 ou 300 notes/vues à gérer en même temps, même sur un iPad taille XXL. Les applications classiques utilisent des interfaces figés de la taille de l'écran, alors que ton projet est une "fenêtre d'édition" se déplaçant sur un écran virtuel de grande taille (la partition au complet).

    La "fenêtre" ne doit gérer que les éléments visibles sur l'écran à un moment donné. Elle doit avoir une liste des Notes visibles et de vues correspondantes. C'est le même principe qu'une tableView qui n'utilise qu'une dizaine de cellules, sans cesse recyclées pour faire croire à un défilement. Les cellules sont toujours les mêmes, mais le contenu change quand l'utilisateur fait défiler les informations. Cela minimise la consommation mémoire de l'application.

    Certains de mes petits camarades font certainement faire la remarque qu'il est plus efficace et plus économique en mémoire de ne PAS EMPLOYER de vues pour les Notes, et d'utiliser les fonctions graphiques (drawrect) pour redessiner l'intégralité de la vue. Techniquement, ils n'ont pas tort. Mais c'est se priver des possibilités d'animations d'iOS qui sont plutôt sympathiques.

    Exemple : ton prototype permet d'effacer une note en cliquant dessus pour afficher un mini-menu. Pas terrible .. Il serait plus fun de placer le doigt sur la Note pour la "jeter" brutalement d'une pichenette. Avec une animation l'image de la Note pourrait traverser l'écran, jusqu'à sortir des limites. Pendant ce temps là, les autres notes pourraient se déplacer aussi pour remplir le trou laissé par l'effacement. C'est visuel, intuitif et amusant..

    Avec les animations tu peux faire "palpiter" une note (en changeant sa taille selon un cycle précis), modifier sa couleur, la faire pivoter sur elle-même, etc .. Les possibilités sont nombreuses.

  • Joanna CarterJoanna Carter Membre, Modérateur

    Si tu utilises un système de indexation comme UITableView, tu aurais :

    struct NoteIndexPath
    {
      var temps: Int
      var note: Int
    }
    

    Donc tu peux trouver le temps, puis le note là dedans.

  • Pour la rapidité de la recherche, je confirme.
    Dans iPocket Draw, les objets "cliqués" sont retrouvés grâce à la position du clic dans la fenêtre et aux coordonnées géométriques de chaque objet et même avec beaucoup d'objets (plusieurs milliers dans un même dessin) c'est rapide.
    ... et pourtant ce n'est que de l'objective-C et pas (encore) du Swift.

  • MayerickMayerick Membre
    27 avril modifié #39

    Bonjour à tous
    Navré pour le retard, malgré les notifications correctement réglées je ne reçois pas les mails quand vous écrivez..

    @Draken, @Eric P.
    Oui en effet je focalise carrément sur cette histoire de boucle, mais je veux bien croire que cela reste très rapide, merci pour le retour en tout cas !

    Je vois l'idée des cellules recyclées, j'avais commencé à faire cela puis voyant que ça compliquait le développement et que les performances n'étaient pas impactée pour le moment j'ai laissé tomber avec l'idée de revenir dessus en cas de problème, un peu dans la logique de "pas d'optimisation prématurée".

    Certains éléments à l'écran seront bien dessinées par drawRect: et d'autres seront des images, mais je vois bien ce que tu veux dire par rapport aux interactions offertes. Petite blague en passant, tu dis: "C'est visuel, intuitif et amusant..", Attention dans l'édition musicale il y a de grandes traditions, et clairement celles-ci n'en font pas partie ! :D Du coup je privilégie le fonctionnel sur le visuel, le second pouvant toujours venir une fois qu'on a clairement défini le premier.

    @Joanna Carter
    Merci pour la suggestion, c'est très intéressant je n'avais pas fait le rapprochement. En effet il me faudrait une sorte de NSIndexPath pour retrouver le chemin d'une note dans modèle[système][ligne][mesure][temps][note]. Quand j'y pense c'est un peu ce que je faisais mais sans utiliser d'objet concret pour garder la référence, je les gardais dans une variable du controller: currentNote, curentTemps, currentMesure.... Tu pourrais s'il te plait développer un peu plus l'usage ?

    Merci pour vos réponse, ça aide vraiment à réfléchir !

  • klogklog Membre

    @Mayerick a dit :
    Bonjour à tous
    Navré pour le retard, malgré les notifications correctement réglées je ne reçois pas les mails quand vous écrivez..

    C'est pareil pour moi depuis le début. J'imagine que le le machin n'est pas activé.

  • @klog

    C'est bon à savoir, merci !

Connectez-vous ou Inscrivez-vous pour répondre.