setNeedsDisplay (Objective C) ne fonctionne pas

MortyMarsMortyMars Membre
mai 2022 modifié dans Objective-C, Swift, C, C++ #1

Bonjour à tous,

Je me heurte à un problème de mise à jour d'une NSView, un plateau d'échiquier, qui ne se fait pas comme je le souhaiterais.
Ainsi, dans le cas d'une partie opposant un Joueur à l'IA, le temps de réaction du Joueur permet que l'affichage de l'échiquier fonctionne correctement et l'on peut visualiser les coups successifs même si parfois deux coups peuvent s'afficher en même temps...
Le véritable problème survient lorsque je fais jouer l'IA contre elle-même : aucun des coups successifs de l'IA (visibles en console) n'est affiché "en direct" à l'écran et ce n'est qu'après le dernier coup (interruption par échec, mat, ou NSAlert) que la mise à jour de la Vue s'effectue, ce qui est juste frustrant...
J'ai intercalé dans mon code des [maVue setNeedsDisplay:YES] entre les coups joués par l'IA
J'ai forcé l'appel de ces setNeedsDisplay dans le tread principal et non éventuellement en arrière plan, par un :

dispatch_async(dispatch_get_main_queue(), ^{
      [maVue setNeedsDisplay:YES];
});
ou par un : 
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(void){
                  [self LancerCoupNoirs];
                  [viewEC setNeedsDisplay:YES];
               });

J'ai retardé l'exécution des coups pour donner à la vue le temps de se rafraichir, par un :

[NSTimer scheduledTimerWithTimeInterval:0.02
                                                           target:self
                                                        selector:@selector(LancerCoupNoirs)
                                                       userInfo:nil
                                                        repeats:NO];

ou par un :

[NSTimer performSelector:@selector(LancerCoupNoirs) withObject:self afterDelay:1];

voire un :

NSTimeInterval delayInSeconds = 1.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
                     [self LancerCoupNoirs];
                  });

J'ai parcouru le net à la recherche de solutions -ce qui m'a d'ailleurs permis de constater que le problème est bien (trop) connu- mais aucune des solutions que j'ai pu me voir proposer (dont celles ci-avant) n'a fonctionné dans mon cas.
Le Grand Central Dispatch (GCD) revient souvent parmi les propositions mais je ne suis souvent parvenu pour ma part qu'à désynchroniser le déroulement du jeu en exécutant par exemple, successivement plusieurs coups Blancs ou Noirs...
À ce stade donc, je sèche désespérément !
Merci de vos expériences éventuelles à ce sujet, ou d'idées nouvelles à explorer.

Mots clés:
«1

Réponses

  • CéroceCéroce Membre, Modérateur
    mai 2022 modifié #2

    Effectivement c'est un problème classique. Tout ce qui est interface utilisateur (IHM) doit être exécuté sur le thread principal. Si les calculs sont effectués sur ce même thread, alors ils bloquent l'affichage, qui ne sera rafraîchi qu'à la fin des calculs.

    La solution est d'effectuer les calculs sur un thread secondaire. Pour cela, on peut utiliser GCD, mais je te conseille l'utilisation de NSOperationQueue et de NSOperation, qui sont de plus haut niveau, donc plus faciles à utiliser et conviennent tout à fait pour ce que tu veux faire.

    L'idée est de créer une NSOperationQueue série (.maxConcurrentOperations = 1) et ensuite d'y ajouter le calcul de chaque "coup" sous la forme d'une NSOperation (vois NSBlockOperation, qui est assez pratique).

    La vraie difficulté sera de communiquer au thread principal que le calcul est terminé. La solution habituelle est d'utiliser une callback: il s'agit d'une closure (= un "block" en ObjC) passée en paramètre, qui sera appelée à l'issue du calcul.
    Enfin, attention! L'appel de la callback sera effectué dans le thread courant, donc celui de l'opération. Cela veut dire que ça ne peut pas directement effectuer des opérations d'interface utilisateur (qui doivent s'exécuter sur le thread principal — bis). Il faudra donc appeler les méthodes d'IHM sur le thread principal:

    dispatch_async(dispatch_get_main_queue(), ^{
        [self.view setNeedsDisplay];
    });
    

    Je ne donne pas beaucoup de code, réclame si besoin.

  • PyrohPyroh Membre

    Bah sinon un tick avec un NSTimer qui se répète et qu'on annule dès que la partie est finie.
    Tu pars avec ça :
    + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;

    Avec un block du genre :

    ^{
        dispatch_async(dispatch_get_main_queue(), ^{
            // Définir quelle couleur doit jouer
            // Faire jouer la couleur
            [self.view setNeedsDisplay];
            // Définir sur la partie est terminée et annuler le timer le cas échéant.
        });
    }
    

    Ça t'évite tout un tas de soucis avec un code toujours synchrone qui manipule le modèle et la mise à jour de la vue.
    Un tick par seconde et tu peux regarder les parties. Tu peux même t'autoriser une petite coquetterie et mettre un des contrôles de vitesse, un bouton pause, etc...

    Tu ferais du Swift je t'aurai conseillé Combine mais bon...

  • Merci à vous deux, Céroce et Pyroh, vous me redonnez l'espoir qui commençait à manquer :)
    Je me plonge vers ces contrées encore inexplorées et vous tiens au courant des avancées ;)

  • CéroceCéroce Membre, Modérateur

    @Pyroh a dit :
    Bah sinon un tick avec un NSTimer qui se répète et qu'on annule dès que la partie est finie.

    Même si ça ne coûte pas grand chose d'essayer, je doute que ça fonctionne. Les NSTimers sont exécutés par la Runloop, et si le thread principal est bloqué, alors les itérations de la runloop sont bloquées.

  • @Cérore et @Pyroh
    Des nouvelles pistes vers lesquelles vous m'avez redirigé, j'ai compris qu'il ne s'agissait pas seulement d'exécuter les méthodes d'IHM dans le thread principal, mais tout aussi essentiellement d'exécuter les méthodes de calcul dans un thread secondaire.
    Le code auquel je suis parvenu est le suivant :

        // Insertion de code dans un thread secondaire 
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            int compteur = 0;
            // Tant qu'aucun des camps n'est Pat ou Mat 
            // et que le compteur n'a pas atteint 20
            while (([Minimax PossibleMovesForSide:sideWhite board:boardEC].count!=0) &&
                   ([Minimax PossibleMovesForSide:sideBlack board:boardEC].count!=0) &&
                   (compteur < 20)) {
    
               // L'IA joue pour les Blancs (qui ne sont ni Pat ni Mat) en AR plan
               [viewEC MakeIAMoveForSide:sideWhite Board:boardEC];
    
               // Mise à jour du ChessView après incursion dans le thread principal
               dispatch_async(dispatch_get_main_queue(), ^{
                  [viewEC setNeedsDisplay:YES];
               });
    
               // Si les Noirs ne se retrouvent pas Pat ou Mat après le coup Blancs...
               if ([Minimax PossibleMovesForSide:sideBlack board:boardEC].count!=0) {
                  // ... alors l'IA joue pour les Noirs en AR plan
                  [viewEC MakeIAMoveForSide:sideBlack Board:boardEC];
    
                  // Mise à jour du ChessView après nouvelle incursion 
                  // dans le thread principal
                  dispatch_async(dispatch_get_main_queue(), ^{
                     [viewEC setNeedsDisplay:YES];
                  });
               }
            }
            compteur++;
         });
    

    Dans le principe ça fonctionne plutôt bien, même si 'esthétiquement' il y aurait lieu de ralentir la succession des coups. Mais malheureusement d'autres soucis apparaissent désormais : une interruption brutale du programme après plusieurs coups, accompagnée d'un message d'erreur :
    "NSWindow drag regions should only be invalidated on the Main Thread!"
    Je suis persuadé que c'est dû à diverses NSAlert présentes dans la méthode gérant les coups de l'IA pour signaler une mise en échec ou un mat, ...NSAlert qui ne supportent pas d'être appelées d'un thread secondaire.
    Je suis donc un peu empêtré dans mon code, hésitant à réécrire certaines méthodes pour gérer ces fameuses NSAlert et/ou les threads, ou à créer des versions 'silencieuses' des méthodes impliquées

    Je n'abandonne pas pour autant vos propositions de code, mais j'avoue ne pas encore être parvenu à les assimiler complètement, puisque j'ai quelques difficultés pour l'instant à comprendre les NSOperation et à gérer efficacement les blocks...

    J'ai donc encore pas mal de choses à régler avant de voir le bout de cette affaire ;)

  • klogklog Membre
    mai 2022 modifié #7

    Tu dois encapsuler tous les appels à l'interface qui auraient lieu dans ton thread secondaire, dans des :

    dispatch_async(dispatch_get_main_queue(), ^{
    ...
    });

  • Merci Klog 😉
    Ça confirme une des solutions envisagées, mais j'ai certaines NSAlert qui utilisent des variables qui elles aussi doivent être encapsulées sans être isolées du reste du code. Ça ne fonctionnera donc pas partout.
    D'où ma réflexion plus globale pour une solution standard
  • PyrohPyroh Membre

    @Céroce a dit :

    @Pyroh a dit :
    Bah sinon un tick avec un NSTimer qui se répète et qu'on annule dès que la partie est finie.

    Même si ça ne coûte pas grand chose d'essayer, je doute que ça fonctionne. Les NSTimers sont exécutés par la Runloop, et si le thread principal est bloqué, alors les itérations de la runloop sont bloquées.

    Tu as totalement raison sur le principe. Mais comme là le soucis était que les actions s'enchainaient sans qu'on ait le temps de les apprécier je suis parti du principe que le main thread n'était pas trop encombré 😉

  • CéroceCéroce Membre, Modérateur

    @MortyMars a dit :smile:
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

    Je ne ferais pas ça: cet appel renvoie une queue système partagée, dont tu connais pas vraiment les caractéristiques.
    Je réitère que tu devrais créer ta propre queue. Tu peux utiliser GCD, mais NSOperationQueue est plus pratique et repose sur GCD de toute façon.
    Ensuite utilise bien une queue série, autrement les coups seront calculés en parallèle, je te laisse imaginer le désastre.

    Dans le principe ça fonctionne plutôt bien, même si 'esthétiquement' il y aurait lieu de ralentir la succession des coups.

    C'était prévisible.

    Je suis persuadé que c'est dû à diverses NSAlert présentes dans la méthode gérant les coups de l'IA pour signaler une mise en échec ou un mat

    Oui c'est le cas. De toute façon, mélanger IHM et code métier est une mauvaise pratique. Mais les séparer est forcément plus difficile car tu vas devoir trouver un moyen de faire communique ces deux domaines. Typiquement tu peux utiliser la délégation, même si dans le code moderne ou utiliserait peut-être des callbacks sous forme de closures ("blocks").

    Je n'abandonne pas pour autant vos propositions de code, mais j'avoue ne pas encore être parvenu à les assimiler complètement, puisque j'ai quelques difficultés pour l'instant à comprendre les NSOperation et à gérer efficacement les blocks...

    Je te montre un exemple très simple:

    @property NSOperationQueue *gameQueue;
    
    -(instancetype) init {
        self = [super init];
    
        _gameQueue = [[NSOperationQueue alloc] init];
        _gameQueue.maxConcurrentOperation = 1; // serial queue
    
        return self;
    }
    
    -(void) queueComputationOfNextMove:(void (^)(void))moveComputation {
        [self.gameQueue addOperation:[NSBlockOperation blockOperationWithBloc: moveComputation]];
    }
    

    J'ai donc encore pas mal de choses à régler avant de voir le bout de cette affaire ;)

    Les closures ne sont pas si difficiles à utiliser mais tu entres dans la programmation concurrente, ce qui n'est jamais simple. L'autre difficulté est la communication des classes.

    P.S.: Très utile:
    http://fuckingblocksyntax.com

  • MortyMarsMortyMars Membre
    mai 2022 modifié #11

    Bonjour à tous,

    @Céroce : Merci pour ta proposition de code sur laquelle j'ai travaillé en créant une variable d'instance NSOperationQueue et en l'initialisant comme tel :

    -(instancetype) init {
          self = [super init];
          maFileSerie = [[NSOperationQueue alloc] init];
          maFileSerie.maxConcurrentOperationCount = 1;   // limiter nb opés à 1 à la fois pour forcer une queue série
          return self;
       }
    

    puis en l'alimentant de coups successifs :

    // Ajout d'une NSOperation coup Blancs
    [self->maFileSerie addOperationWithBlock:^{
             sleep(1);
             [self SilentMakeIAMoveForSide:sideWhite Board:boardEC];
             // Forcement du thread principal pour MàJ ChessView et liste coups
             dispatch_async(dispatch_get_main_queue(), ^{
                      [viewEC setNeedsDisplay:YES];
                      [monMCNControleur MaJtxtCoups];
              });
    }];
    

    J'ai également dû créer des copies de pas mal de mes méthodes pour les rendre "silencieuses" (vis-à-vis d'appels à NSAlert) quand je ne parvenais pas à les appeler dans le thread principal.
    À ce stade je suis plutôt satisfait du résultat.
    Mais il reste néanmoins une méthode récalcitrante, appelée en fin de procédure IA vs IA, qui gâche un peu la fête car je ne parviens pas à l'appeler sur le thread principal sans passer par une refonte trop profonde du code commun avec d'autres fonctionnalités sur lesquelles je ne souhaite pas revenir...
    La solution pour que cela fonctionne serait peut-être que je conditionne l'exécution d'une partie du code de cette méthode au thread dans lequel il s'exécute.
    D'où la question suivante : existe t-il un moyen d'implémenter un test s'appuyant sur le fait que l'on est dans le thread principal ou non ?

  • CéroceCéroce Membre, Modérateur

    @MortyMars a dit :

    D'où la question suivante : existe t-il un moyen d'implémenter un test s'appuyant sur le fait que l'on est dans le thread principal ou non ?

    Ça existe: [NSThread isMainThread].

    Cependant, les seules fois où je l'ai vu était dans des assertions:

    -(void) doSomeGUIThing {
        NSAssert([NSThread isMainThread]);
        …
    }
    

    Ce qui d'ailleurs n'est plus très utile de nos jours puisque il y a normalement des alertes si on exécute du code AppKit ou Core Graphics sur un thread secondaire.

    Je ne cerne pas bien ton problème. Ton moteur de calcul des coups doit de toute façon être séparé de l'affichage. Le calcul va devoir informer l'IHM, c'est pourquoi je te parlais de délégation:

    @protocol ChessEngineDelegate 
    
    -(void) chessEngine:(ChessEngine *)sender didMovePiece:(Piece *)piece toPosition:(Position *)position;
    
    @end
    

    Dans mon exemple, ChessEngine va appeler la méthode sur son thread (secondaire). Dans le délégué, qui s'occupe de l'IHM:

    @implementation ChessboardViewController
    
    -(void) chessEngine:(ChessEngine *)sender didMovePiece:(Piece *)piece toPosition:(Position *)position {
        dispatch_async(dispatch_get_main_queue(), ^{
            [self movePiece:piece toPosition:position];
        });
    }
    
  • PyrohPyroh Membre

    @Céroce a dit :

    @MortyMars a dit :

    D'où la question suivante : existe t-il un moyen d'implémenter un test s'appuyant sur le fait que l'on est dans le thread principal ou non ?

    Ça existe: [NSThread isMainThread].

    Cependant, les seules fois où je l'ai vu était dans des assertions:

    -(void) doSomeGUIThing {
      NSAssert([NSThread isMainThread]);
      …
    }
    

    Ce qui d'ailleurs n'est plus très utile de nos jours puisque il y a normalement des alertes si on exécute du code AppKit ou Core Graphics sur un thread secondaire.

    Wopopop ! On mélange thread et queue ici, c'est pas pareil. Il faut oublier NSThread si on utilise GCD c'est le plus simple.

    Plus concrètement @MortyMars c'est toi qui définit l'API donc c'est toi qui sait depuis quelle queue les appels sont réalisés. Généralement tu n'en viens à vérifier sur quel thread/queue le code est exécuté que dans ces conditions :

    • une API first ou third party ne fonctionne pas comme documenté et tu veux debugger en profondeur.
    • on est en 2007 et GCD n'est pas encore disponible.

    J'ai pas tout relu mais il me semble que @Céroce t'as donné la marche à suivre. Mais je vais formaliser: tu dois créer un moteur qui va s'occuper exclusivement du calcul des coups. C'est simplement une classe qui contient une représentation du plateau de jeu (un tableau à deux dimension je suppose), une OperationQueue série et les méthodes qui modélisent ton algorithme de jeu.

    Certaines de ces méthodes sont publiques et tiennent lieu d'API. Elle permettent à l'UI d'informer des movements de pièces produites par le joueur (CPU v. CPU c'est autre chose), d'une fonction reset de plateau, éventuellement l'annulation de coup, une fonction pause, etc...

    Pour avertir l'UI d'un changement de l'état du plateau il faut utiliser la délégation. C'est généralement le view controller qui tien lieu de délégué. Là je te renvoie au post de @Céroce juste au dessus.

    Maintenant on résume :

    HUMAN vs. CPU
    1. L'UI informe le moteur d'un mouvement du joueur
    2. Le moteur calcule sur la queue la validité du coup
    3. Le moteur informe de la validité du coup via le délégué
    4. Si invalide on informe le joueur et on met à jour l'UI en prenant bien soin d'exécuter le tout sur la main queue via dispatch_async(dispatch_get_main_queue(), ^{ ... } puis on reviens en 1. Si valide on bloque la possibilité pour le joueur de déplacer des pièces (sur la main queue aussi).
    5. Le coup pour le CPU est calculé sur la queue du moteur.
    6. Une fois le calcul terminé on informe l'UI du coup du CPU via le délégué et on dispatche sur la main queue la fonction de mise à jour du plateau.
    7. Le moteur calcule la condition de victoire et informe le délégué du résultat. Si le résultat est nul, on débloque la possibilité de manipuler les pièces et on revient en 1. Sinon on gère l'issue de la partie depuis la main queue et on met le jeu dans un mode qui impose un reset.

    CPU vs. CPU
    1. L'UI informe le moteur qu'il faut commencer à jouer.
    2. Le moteur calcule sur la queue le coup du CPU1.
    3. Le moteur informe le délégué du coup joué. L'UI est mise à jour sur la main queue.
    4. Éventuellement le moteur fait une pause de quelques seconde histoire de voir les coups s'enchaîner.
    5. Le moteur calcule sur la queue le coup du CPU2.
    6. Le moteur informe le délégué du coup joué. L'UI est mise à jour sur la main queue.
    7. Le moteur calcule la condition de victoire et informe le délégué du résultat. Si le résultat est nul, éventuellement le moteur fait une pause de quelques seconde histoire de voir les coups s'enchaîner et on revient en 2. Sinon on gère l'issue de la partie depuis la main queue et on met le jeu dans un mode qui impose un reset.

    Pour séparer correctement UI et moteur je te conseille de conserver une modélisation du plateau coté moteur et côté UI. Au départ elles sont identiques et correspondent au placement de départ des pièces. Ensuite le moteur manipulera sa propre modélisation et informera des changement via le délégué. Là la modélisation coté UI est mise à jour et l'affichage est mis à jour sur base de ces données. Mais j'avais l'UI ne doit manipuler elle-même sa modélisation du plateau !

    Ça peut paraître contre-intuitif de dupliquer de la données mais ça va te forcer à vraiment considérer moteur et UI comme deux parties distinctes qui ne font que s'échanger des commandes et se communiquer des données. C'est un très bon exercice.

  • Bonjour à tous, et merci à Céroce et Pyroh pour leurs retours toujours détaillés et didactiques :)

    @Céroce :

    Ça existe: [NSThread isMainThread].

    Merci !
    C'est pile poil ce que j'espérais mais que j'ai été incapable d'exhumer du net. Mon Google ne s'est pas montré coopératif sur ce coup...
    Je vais tester ça ;)

    Ton moteur de calcul des coups doit de toute façon être séparé de l'affichage.

    C'est le cas, ce sont deux classes distinctes : 'Minimax' pour le moteur et 'ChessView' pour l'affichage et la MàJ du plateau.
    Grâce à ton aide 'ChessView' est convenablement gérée par NSOperationQueue et NSOperation
    Mais par choix perso, quelques boites de dialogues (NSAlert) "agrémentent" le moteur (classe 'Minimax') pour signaler par exemple une mise en échec, un mat, ou un pat ; et c'est l'affichage de ces boites de dialogues qui ne sont pas forcément exécutées dans le thread ppal qui plante le programme.
    J'en ai désactivé une grande partie en créant des copies de méthodes pour les rendre "silencieuses", mais il reste une méthode récalcitrante que je ne parviens pas à aménager sans remettre en cause les fonctionnalités de bases (Joueur vs IA notamment)

    Le calcul va devoir informer l'IHM, c'est pourquoi je te parlais de délégation:

    @protocol ChessEngineDelegate 
    
    -(void) chessEngine:(ChessEngine *)sender didMovePiece:(Piece *)piece toPosition:(Position *)position;
    
    @end
    

    J'ai semble t-il zappé cette piste concernant la délégation, que je ne maitrise d'ailleurs pas (non plus)
    Je mets ça en haut de la pile car je sens que tu vas me suggérer de reporter les NSAlert évoquées plus haut de 'Minimax' vers 'ChessView' par un processus de délégation, et j'en ai d'avance des sueurs froides :'( :# ;)

    @pyroh

    • une API first ou third party ne fonctionne pas comme documenté et tu veux debugger en profondeur.

    Ça n'est pas mon cas, je le jure ;)...

    • on est en 2007 et GCD n'est pas encore disponible.

    ... et on est bien en 2022 ;) où le GCD me file des boutons :#

    J'ai pas tout relu mais il me semble que @Céroce t'as donné la marche à suivre

    Ne dis pas que ça vient de moi, mais il est vraiment trop fort ;) (lui aussi ;) ;) )

    Maintenant on résume :
    HUMAN vs. CPU

    Cette fonctionnalité de base de l'appli fonctionne plutôt conformément à mes attentes (à la performance du moteur près), d'où ma réticence à reprendre le code jusqu'à la remettre en cause.

    CPU vs. CPU
    1. L'UI informe le moteur qu'il faut commencer à jouer.
    2. Le moteur calcule sur la queue le coup du CPU1.
    3. Le moteur informe le délégué du coup joué. L'UI est mise à jour sur la main queue.
    4. Éventuellement le moteur fait une pause de quelques seconde histoire de voir les coups s'enchaîner.
    5. Le moteur calcule sur la queue le coup du CPU2.
    6. Le moteur informe le délégué du coup joué. L'UI est mise à jour sur la main queue.

    Hors délégation, le fonctionnement est conforme à cette séquence

    1. Le moteur calcule la condition de victoire et informe le délégué du résultat. Si le résultat est nul, éventuellement le moteur fait une pause de quelques seconde histoire de voir les coups s'enchaîner et on revient en 2. Sinon on gère l'issue de la partie depuis la main queue et on met le jeu dans un mode qui impose un reset.

    La condition de victoire est vérifiée avant et après chaque coup, ce qui génère des messages d'alerte (c'est un choix perso) qui, finalement, posent problème lorsqu'ils sont appelés d'un thread secondaire...

    Pour séparer correctement UI et moteur je te conseille de conserver une modélisation du plateau coté moteur et côté UI

    C'est le cas je pense, car ce sont deux classes séparées mais communiquant entre elles : ChessBoard pour les données et ChessView pour l'UI

    Ça peut paraître contre-intuitif de dupliquer de la données mais ça va te forcer à vraiment considérer moteur et UI comme deux parties distinctes qui ne font que s'échanger des commandes et se communiquer des données. C'est un très bon exercice.

    Je note le conseil ;)

    Dans un premier temps, compte tenu de la facilité de mise en oeuvre, je vais tenter d'exploiter [NSThread isMainThread] et voir si je peux m'en sortir ainsi, même si je dois faire quelques concessions vis-à-vis de l'élégance du code B)
    Sinon je crains de devoir mettre les mains dans le cambouis de la délégation...
    Je donnerai des nouvelles.
    Merci encore à vous deux :)

  • Bonjour à tous,

    @Céroce et Pyroh :
    Concernant ma 'méthode récalcitrante', résolue provisoirement par le biais d'un test sur [NSThread isMainThread], je dois avouer qu'elle masquait un problème bcp plus élémentaire (shame, shame, shame, shame on me) : dans le module IA vs IA, ma boucle de succession des coups Blancs puis Noirs (ou inversement) ne s'interrompait jamais dès un Mat ou un Pat car mon 'While' testait dans le thread principal des variables modifiées dans un thread secondaire, ce qui ne pouvait évidemment pas marcher... :#
    Il a suffit d'intégrer la boucle while dans le même thread que les coups successifs pour que ce dysfonctionnement disparaisse.
    [NSThread isMainThread] aura eu très indirectement le mérite de cette mise en évidence.
    Le module IA vs IA est désormais fonctionnel, si ce n'est qu'il est pour l'instant privé -a minima- d'une boite de dialogue NSAlert (du moteur) indiquant sur quoi se termine la partie.
    Pour Ie reste, il me faut donc désormais me plonger dans les méandres de la délégation pour voir comment résoudre cette petite contrariété, dans la mesure où il s'avérera que ça en vaut la peine ;)

  • CéroceCéroce Membre, Modérateur

    La délégation, tout le monde s'en fait des montagnes; j'ai donné des formations au développement iOS plusieurs années, je parle d'expérience. Mais franchement ce n'est pas compliqué, le plus difficile est de comprendre qui fait quoi. D'ailleurs souvent, on l'a déjà utilisé sans se poser de questions.

    Pour donner un exemple, ton application comprend certainement une classe AppDelegate, qui se conforme au protocole NSApplicationDelegate:

    @interface AppDelegate <NSApplicationDelegate>
    
    @end
    

    NSApplication possède également une propriété:

    @property(weak) id<NSApplicationDelegate> delegate;
    

    Ça veut dire que l'application va avertir son délégué (delegate) quand certains événements se produisent. Les messages qui avertissent sont contractualisés par le protocole NSApplicationDelegate. Par exemple:

    - (void)applicationDidFinishLaunching:(NSNotification *)notification;
    

    est appelé par l'application pour dire à son délégué qu'elle a fini de se lancer.

    Pour récapituler, c'est bien l'application qui délègue une partie de son fonctionnement à un délégué (ici l'AppDelegate).

  • CéroceCéroce Membre, Modérateur

    Pour ton moteur c'est pareil. ChessBoard va déléguer une partie de son fonctionnement à son délégué:

    @class ChessBoard; // Juste pour dire qu'une classe "ChessBoard" existe avant de déclarer le protocole, autrement le compilateur se plaint.
    
    
    @protocol ChessBoardDelegate 
    
    // Une méthode qui sera appelée sur 'delegate' quand la pièce a été déplacée
    -(void) chessBoard:(ChessBoard *)sender didMovePiece:(Piece *)piece toPosition:(Position *)position;
    
    @end
    
    
    @interface ChessBoard: NSObject
    
    // Important: les délégués sont toujours déclarés weak pour éviter les cycles de rétention (retain cycles)
    @property (weak) id <ChessBoardDelegate> delegate;
    
    @end
    

    Au niveau de l'implémentation, dans ChessBoard:

    // Quand le moteur a calculé que la pièce s'est déplacée:
    [self.delegate chessBoard:self didMovePiece:piece toPosition:position];
    

    Dans ChessView:

    @interface ChessView: NSView <ChessBoardDelegate>
    
    @end
    
    
    @implementation ChessView
    
    -(void) chessBoard:(ChessBoard *)sender didMovePiece:(Piece *)piece toPosition:(Position *)position {
        // Sur la mainQueue, mettre à jour l'affichage des pièces
    }
    
    @end
    

    Et pour finir, il faut dire QUI est le délégué de ChessBoard (par exemple dans le WindowController):

    chessBoard.delegate = chessView;
    

    Si on ne le fait pas, il ne se passe rien: le message sera envoyé à self.delegate qui est nil, et en ObjC, envoyer un message à nil n'a pas d'effet (pas même un plantage).

  • @Céroce a dit :
    La délégation, tout le monde s'en fait des montagnes; j'ai donné des formations au développement iOS plusieurs années, je parle d'expérience. Mais franchement ce n'est pas compliqué, le plus difficile est de comprendre qui fait quoi. D'ailleurs souvent, on l'a déjà utilisé sans se poser de questions.

    Cela devait être plus facile dans le temps avec UIAlertView & UIAlertViewDelegate, un exemple simple et compréhensible je trouve.

    Je vais rajouter que le delegate, c'est vraiment de la délégation. C'est un moyen de faire communiquer 2 objets sur un « plan similaire » (Objet A possède ObjetB, ou ObjectC possède ObjetA et ObjetB souvent).
    Cela permet à un objet de signifier tous les moments importants de sa vie (ah tien, on a cliqué à tel endroit, on est arrivé à telle montant, etc. ). Tu avertis ton delegate, et s'il considère qu'il faut réagir ou non. Un peu comme si à chaque étape de ton travail, tu devais dire où tu en es à ton supérieur. Il te dira peut-être "c'est bien", "continue", ou "stop", fais autre chose, etc en fonction des étapes.

    En Swift, la délégation est de plus en plus oubliée au profit des blocks/closures (qu'on pourrait également faire en Objective-C d'ailleurs, j'ai eu un collègue fan). Cela a ses avantages et ses inconvénients.

  • Merci à vous deux, Céroce et Larme pour les exemples de codes et les précisions apportées.
    Pour mon cas précisément, puisqu'il s'agit de NSAlert de ma classe Minimax, qui posent problème quand elles ne sont pas appelées dans le thread ppal, je pense que je vais transposer le code de Cérore en faisant de Minimax le délégant, et effectivement ChessView le délégué.
    Le processus me parait, à ce stade précédant l'implémentation ;) plutôt clair.
    Cependant il reste une interrogation concernant la dernière précision de Céroce

    @Céroce a dit :
    Et pour finir, il faut dire QUI est le délégué de ChessBoard (par exemple dans le WindowController):

    chessBoard.delegate = chessView;
    

    Si on ne le fait pas, il ne se passe rien: le message sera envoyé à self.delegate qui est nil, et en ObjC, envoyer un message à nil n'a pas d'effet (pas même un plantage).

    Quand tu dis de placer cette déclaration dans le WindowController, s'agit-il bien de ma classe Contrôleur ?
    Et si oui pourquoi dans cette classe et pas dans ChessBoard (de ton exemple) ou ChessView, ce qui me paraitrait plus approprié ?

  • Une vue ne doit pas être un délégué sauf dans quelques cas particuliers.
    Dans l'idéal c'est le contrôleur qui est le délégue. Ce qui fait que quand un truc se passe de la vue vers le moteur :
    Input utilisateur dans la vue -> Contrôleur -> Mise à jour du moteur
    et du moteur vers la vue :
    Event moteur -> Contrôleur -> Mise à jour de la vue

    Les classes de vue (ici ChessBoard) et de modèle (le moteur) ne doivent pas se connaitre. C'est le contrôleur qui connait les deux et qui les lie. De cette manière tu peux changer ta vue ou ton moteur sans que l'autre ne soit impacté.

  • C'est toute la logique de l'analogie avec un travailleur et son supérieur.
    Est-ce toi qui va pendre les décisions/réagir ? Alors, tu es ton propre delegate.
    Est-ce ton supérieur qui va prendre les décisions/réagir ? Alors, c'est ton delegate.
    Ici, en architecture MVC, c'est souvent le "C" de Controller, qui contrôlera.

  • CéroceCéroce Membre, Modérateur

    @MortyMars a dit:
    Quand tu dis de placer cette déclaration dans le WindowController, s'agit-il bien de ma classe Contrôleur ?

    Je ne peux pas te répondre, ne sachant pas ce qu'est ta classe Contrôleur.
    Historiquement, sur Mac on utilise des NSWindowController pour gérer une fenêtre, mais depuis quelques années, les NSViewControllers existent également sur macOS. Donc, tu vois quelle est la bonne classe dans ton code.

    Et si oui pourquoi dans cette classe et pas dans ChessBoard (de ton exemple) ou ChessView, ce qui me paraitrait plus approprié ?

    Parce qu'il faut que l'objet qui fait l'affectation ait accès à la fois à la ChessBoard et à la ChessView.

    @Pyroh a dit :
    Une vue ne doit pas être un délégué sauf dans quelques cas particuliers.
    Dans l'idéal c'est le contrôleur qui est le délégue. Ce qui fait que quand un truc se passe de la vue vers le moteur

    Je suis d'accord, et j'aurais dû t'orienter vers cette architecture:

    • Le contrôleur est délégué de ChessBoard et implémente donc ChessBoardDelegate. (et non pas ChessView comme je te l'avais indiqué).
    • Quand il reçoit un message de délégation, il envoie alors un message à la ChessView.
  • Merci à vous trois, Pyroh, Larme et Céroce :)

    Au stade de l'implémentation je constate que (pour moi) la délégation n'est pas aussi simple ... :#

    1) J'ai eu plusieurs réactions inattendues comme des Protocoles non reconnus par le compilateur ; et l'ajout inconsidéré d'importation de fichiers d'entête pour tenter de résoudre le souci, a mis le bazar un temps dans le projet...
    Je m'en suis sorti par une pirouette en repositionnant quelques #import et en déclarant quelques @class...

    2) La délégation se faisant -au niveau du code- entre instances et non entre classes proprement dites, elle ne pourra pas s'appliquer au moteur, qui est une classe sans instances créées et définissant uniquement des Méthodes de Classe.
    Mais je peux effectivement la mettre en place entre ma Vue et mon Contrôleur : c'est ce sur quoi je travaille en ce moment.

    3) Enfin, quand il s'agit de définir qui est le délégué de qui, la déclaration...

    @Céroce a dit :

    chessBoard.delegate = chessView;
    

    ...devra être faite après instanciation des deux objets concernés, ce qui complique un peu plus le fait de trouver le bon endroit pour le faire.

    Grâce à vous j'ai tous les outils en mains pour avancer : merci encore
    Comme d'hab je donnerai des nouvelles, qui seront j'espère celles du succès de l'opération B)

  • Bonjour à tous,

    Nos vies sont balisées par les grands évènements auxquels on se confronte : un premier cri, un premier pas, un premier pote, une première pote, et vice-versa, ... et pour les plus chanceux une délégation de classe en Objective C :D
    Je fais partie des chanceux car, oui, je viens de mettre en place mon premier protocole de délégation Obj-C :D :D

    Ma classe délégante :

    ChessView.h
    #import <Cocoa/Cocoa.h>
    #import "AppDelegate.h"
    #import "MCNconnecteur.h"
    
    @class ChessView;
    @protocol ChessViewDelegate <NSObject>
       -(NSString *)AlerteEchecRoiSide:(Side)side;
    @end
    
    @interface ChessView : NSView <ChessViewDelegate>
       ...
       @property (weak) id <ChessViewDelegate> delegate;
       ...
    @end
    
    

    Ma classe déléguée :

    MCNconnecteur.h
    #import <Foundation/Foundation.h>
    #import "Minimax.h"
    #import "Util.h"
    //#import "ChessView.h" // cet import fait tout planter....
    
    @class ChessView;
    @protocol ChessViewDelegate;
    
    @interface MCNconnecteur : NSObjectController <ChessViewDelegate>
       ...
       /* Déclaration des Méthodes gérant les NSAlert déléguées */
       -(void) AlerteEchecRoiSide:(Side) side;
       ...
    @end
    
    

    ainsi que :

    MCNconnecteur.m
    // CLASSE CONTROLEUR INTERAGISSANT SUR L'UI ET SES DIFFERENTS OBJETS
    #import "MCNconnecteur.h"
    
    @implementation MCNconnecteur
    
       //***************************************************
       // Méthode d'affichage d'une NSAlert, déléguée
       //
       -(void)AlerteEchecRoiSide:(Side)side {
    
          NSString *msgTitre;
          NSString *msgInfo;
          int bouton;
    
          if (sideCourant == sideWhite) {
             msgTitre = @"Le Roi NOIR est en position d'Échec !";
             msgInfo  = @"OK pour poursuivre la partie...";
          }
          else {
             msgTitre = @"Le Roi BLANC est en position d'Échec !";
             msgInfo  = @"OK pour poursuivre la partie...";
          }
    
          NSAlert *alertEchec = [[NSAlert alloc] init];
          [alertEchec addButtonWithTitle:@"OK (Méthode déléguée)"];
          [alertEchec setMessageText:msgTitre];
          [alertEchec setInformativeText:msgInfo];
          [alertEchec setAlertStyle:NSAlertStyleInformational];
    
          // Attente clic OK
          NSModalResponse boutonChoisi = [alertEchec runModal];
          if (boutonChoisi == NSAlertFirstButtonReturn) bouton = 1; /* ce qui ne sert à rien d'autre
          qu'interrompre le programme en attente d'un clic sur 'OK', bouton = 1 n'est qu'un artifice */
    
       }
    
    @end
    
    
    

    La déclaration de lien de délégation :

    AppDelegate.m
    #import "AppDelegate.h"
    #import "ChessView.h"
    
    @implementation AppDelegate
    
       //***********************************************************
       // Unique méthode (d'instance) de la classe
       // ayant pour objectif d'initialiser l'application
       - (void)applicationDidFinishLaunching:(NSNotification *)aNotification
       {
          // Insert code here to initialize your application
          ... 
          monMCNControleur.maChessView.delegate = monMCNControleur;
         ...
       } 
    
    @end
    

    Et enfin, l'appel de la méthode via la classe délégante :

    Appel à Méthode déléguée par ChessView à MCNconnecteur */
             [monMCNControleur.maChessView.delegate AlerteEchecRoiSide:otherSide];
    

    Ça fonctionne...
    Même si ici ça n'a qu'une utilité purement pédagogique, car plutôt que d'appeler la méthode (déclarée) de la classe délégante, j'ai plus intérêt à appeler directement sa jumelle (définie) de la classe déléguée.
    Je me dis que tout ça c'est pour le fun, et que peut-être je saisirai un peu plus tard toute la subtilité de ce protocole et les possibilités qu'il offre réellement B)

  • CéroceCéroce Membre, Modérateur

    @MortyMars félicitations!
    Tu verras que ça va devenir naturel, quand tu auras bien établi quel objet délègue et lequel est le délégué.

    Au départ, on peut se demander pourquoi déclarer un protocole, mais ça sert à formaliser les choses. Aussi l'objet qui délègue ne sait rien de son délégué (défini avec le type générique id) si ce n'est qu'il se conforme au protocole. C'est l'application d'un des principes SOLID (inversion de contrôle). Ça permet de découpler la classe qui délègue de la classe déléguée.

  • J'ai malgré tout deux messages d'erreur, non bloquant, lors de la compilation, dont je n'arrive pas à me débarrasser :

    • Xcode signale au niveau de @interface MCNconnecteur : NSObjectController un "Cannot find protocol definition for 'ChessViewDelegate'"
    • et au niveau de @implementation ChessView un "Class 'ChessView' does not conform to protocol 'ChessViewDelegate'"
      Encore une fois, ça compile et fonctionne et je peux donc vivre avec, mais il y a tout de même qqchose qui n'est pas optimal...
      Je n'ai rien trouvé d'interessant ou de correspondant sur le net.
      Quelqu'un aurait déjà eu ça ?
  • CéroceCéroce Membre, Modérateur

    @MortyMars a dit :

    • Xcode signale au niveau de @interface MCNconnecteur : NSObjectController un "Cannot find protocol definition for 'ChessViewDelegate'"

    Dans MCNConnecteur.h:

    #import "ChessView.h"
    
    • et au niveau de @implementation ChessView un "Class 'ChessView' does not conform to protocol 'ChessViewDelegate'"

    Tu as dû commettre une erreur, parce qu'il n'y a aucune raison que ChessView se conforme à ChessViewDelegate. C'est son délégué (MCNConnecteur) qui doit s'y conformer.

  • Bonjour Céroce,
    Oui, tu as raison dans l'entête de ChessView, j'avais :

    @interface ChessView : NSView <ChessViewDelegate>
    

    Ce qui suppose que ChessView se conforme au protocole de délégation de ... ChessView
    J'ai corrigé et le message d'erreur correspondant a disparu ;)

    Pour le second problème, dans l'entête MCNconnecteur.h j'ai ajouté : #import "ChessView.h"
    Et dans l'entête ChessView.h j'ai supprimé : #import "MCNconnecteur.h" pour éviter les importations en boucle, et l'ai 'remplacé' par @class MCNconnecteur

    MCNconnecteur.h
    #import <Foundation/Foundation.h>
    #import "Minimax.h"
    #import "Util.h"
    #import "ChessView.h"
    
    @class ChessView;
    @protocol ChessViewDelegate;
    
    @interface MCNconnecteur : NSObjectController <ChessViewDelegate>
       ...
       /* Déclaration des Méthodes gérant les NSAlert déléguées */
       -(void) AlerteEchecRoiSide:(Side) side;
       ...
    @end
    
    

    et

    ChessView.h
    #import <Cocoa/Cocoa.h>
    #import "AppDelegate.h"
    
    @class MCNconnecteur;
    @class ChessView;
    @protocol ChessViewDelegate <NSObject>
       -(NSString *)AlerteEchecRoiSide:(Side)side;
    @end
    
    @interface ChessView : NSView
       ...
       @property (weak) id <ChessViewDelegate> delegate;
       ...
    @end
    
    

    Ça compile toujours mais j'ai également toujours dans l'entête MCNconnecteur.h le message de Xcode signalant au niveau de

    @interface MCNconnecteur : NSObjectController <ChessViewDelegate>
    

    un " Cannot find protocol definition for 'ChessViewDelegate' " :#

  • CéroceCéroce Membre, Modérateur

    Dans MCNConnecteur.h, retire

    @protocol ChessViewDelegate;
    

    En effet cette déclaration signale au compilateur: "sache qu'il existe un protocole qui s'appelle ChessViewDelegate". Or ça ne sert à rien puisque celui-ci est déclaré dans ChessView.h que MCNConnecteur.h importe.

    Deuxièmement, dans ChessView.h, retire:

    @class MCNconnecteur;
    

    Parce que ChessView n'a pas besoin de savoir qu'il existe une classe MCNConnecteur. Elle doit juste savoir que son délégué se conforme à ChessViewDelegate.

    Je ne suis pas sûr que ça règlera tous les problèmes, aussi il est important que tu comprennes la logique pour les régler toi-même.

  • Dans ChessView.h, je peux effectivement supprimer @class MCNconnecteur;

    Mais si je supprime dans MCNConnecteur.h, @protocol ChessViewDelegate;
    alors j'ai carrément un "Build Failed"...

  • CéroceCéroce Membre, Modérateur
    juin 2022 modifié #31

    @MortyMars a dit :

    Mais si je supprime dans MCNConnecteur.h, @protocol ChessViewDelegate;
    alors j'ai carrément un "Build Failed"...

    Alors vérifie ce que tu as tapé. Si le protocole est défini dans ChessView.h et qu'il y a bien un #import ChessView.h dans MCNConnecteur.h, alors ça doit fonctionner.

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