Recherche via un UITextField

Hello !


 


Voilà  mon problème : j'ai un UITextField qui lance une recherche, en fonction de ce qui est tapé. Voilà  mon premier jet de code :



- (void)textFieldDidChange:(NSNotification*)aNotification
{
NSString * text = self.tokenSearchField.inputTextField.text ;

if (text.length == 0)
{
[self collapseTheTableView] ;
return;
}

self.patternSearched = text ;
[self filterWithText:text] ;
[self.tableView reloadData] ;
}

Le problème c'est que c'est trop gourmand en ressources en qu'en conséquence, mon clavier répond mal (si je tape vite, toutes les lettres ne sont pas tapées ; par exemple, si je tape vite "bonjour", il rentrera dans le text field "bnjr").


 


Je pense que je vais donc devoir faire de l'asynchrone.


 


Avant de me lancer, avez-vous des conseils/pattern/"librairies à  aller voir" à  me donner ?


 


Merci !


Réponses

  • Tu sais où c'est long exactement ? Avec les instruments ?


  • J'ai pas testé mais je pense que c'est 



    [self filterWithText:text] ;

    (qui parcourt un NSArray)


     


    Je suis en train de monter une solution avec les idées suivantes :


    - si le nouveau texte à  tester est un sur text du précédent, on peut utiliser le "bout de recherche" précédent


    - les recherches sont lancées en async


    - quand on lance une nouvelle recherche avant on stoppe la précédente.


  • C'est difficile de répondre car j'ai pas le souvenir d'avoir eu des ralentissements lors d'une recherche. Je pense que l'idéal pour toi c'est de voir où exactement tu perds du temps dans ta méthode de filtre. 


     


    Tu filtres comment d'ailleurs ?


  • colas_colas_ Membre
    juin 2015 modifié #5

    Mon filtre est fou bête : j'itère le long du NSArray et je garde ce qui matche. Je recherche une sous-string dans ses strings.


     


    Après optimisation, c'est pas vraiment mieux. Je vais voir si c'est pas un des composants que j'utilise qui ralentit tout.


     


    EDIT : non cela vient de ma méthode de recherche.


    Même après optimisation, si je tape vite, des lettres passent à  la trappe.

  • colas_colas_ Membre
    juin 2015 modifié #6

    @Magiic je n'ai encore jamais réussi à  utiliser instruments (trop compliqué pour moi). Aurais-tu des conseils/tutos à  me donner ?


  • EDIT :


     


    il semble que cette fois-ci ça ne lague plus. Je vais donc reporter à  plus tard l'optimisation (si nécessaire)


  • CéroceCéroce Membre, Modérateur

    @Magiic je n'ai encore jamais réussi à  utiliser instruments (trop compliqué pour moi). Aurais-tu des conseils/tutos à  me donner ?

    Regarde l'Aide. Elle montre quelques cas simples d'utilisation. (En fait, je trouve l'aide plutôt incomplète, mais les bases sont expliquées clairement).
  • AliGatorAliGator Membre, Modérateur

    Je vais donc reporter à  plus tard l'optimisation (si nécessaire)

    YAGNI ! No-premature-optimizations ! C'est bien ça :)

    Bon sinon dans les idées si tu y reviens, évidemment le mieux est d'optimiser ta méthode de filtrage/recherche, mais si elle reste longue malgré ça (parce que malgré toutes tes optimisations de toute façon il y a un appel à  un WebService ou le jeu de données est trop important, etc), le plus classique :

    1) Lancer le filtrage en asynchrone évidemment:

    dispatch_async(dispatch_get_global_queue(...), ^{
    [self filterWithText:text];
    dispatch_async(dispatch_get_main_queue(), ^{
    [self.tableView reloadData];
    });
    });
    Attention commandant, ta méthode filterWithText n'est pas stateless (ce n'est pas très "fonctionnel programming tout ça ^^), c'est à  dire que je suppose qu'elle va changer l'état interne de ta classe (modifier une @property). Ca peut poser des problèmes et complexifier le debug, en particulier si tu as plusieurs appels à  filterWithText en parallèle qui modifient tous en même temps ton objet, surtout si le résultat n'arrive pas dans le même ordre que l'appel...
    Je te conseillerai donc plutôt une méthode "-(NSArray*)filteredResultsWithText" qui elle est stateless (ne modifie pas ton objet self, mais retourne le tableau des objets filtrés), quitte à  ce qu'ensuite tu fasses un "self.filteredResults = [self filteredResultsWithText]" juste avant de faire ton reloadData.
    Ca n'a l'air de rien, en soi ça ne va pas corriger ton risque de désordonnancement de tes résultats, mais c'est déjà  plus propre.


    2) ne lancer le filtrage que si l'utilisateur a arrêté de tapé pendant X secondes. Pas la peine de lancer un filtrage à  chaque nouvelle lettre s'il est en plein en train de taper rapidement un mot complet, autant attendre qu'il ait fini de taper le mot complet. Ca non plus ce n'est pas très dur à  implémenter, par exemple (entre autres solutions) avec des choses comme "-performSelector:withObject:afterDelay:" pour ne lancer la méthode de filtrage qu'au bout d'un délai X, et "+cancelPreviousPerformRequestWithTarget:" pour l'annuler. Ainsi, à  chaque nouveau caractère dans ton textField tu "performSelector:withObject:AfterDelay:" pour dire "lance moi le filtrage dans 0.3 secondes" par exemple, mais avant tu "+cancelPreviousPerformRequestWithTarget:" pour annuler toute execution du filtrage qui avait été préalablement planifiée, et ainsi si l'utilisateur tape un nouveau caractère dans les 0.3 secondes, ça ne va finalement pas faire le filtrage et attendre de nouveau 0.3s avant de faire le prochain, etc.
  • @Ali :


     


    -> oui j'ai appliqué YAGNI!


     


    -> merci pour ces bonnes idées, surtout celle du performWithDelay.


     


    -> pour l'instant effectivement, j'ai une recherche qui n'est pas stateless (elle utilise les recherches précédentes). Cependant, avant de lancer une recherche j'annule toujours la précédente. Mais je retiens ce principe de prendre avec des pincettes les statefull methods


     


    Pour info, voici mon code:



    - (void)filterWithText:(NSString *)text
    {
    /*
    First, we cancel the previous research
    */
    [self.researchQueue cancelAllOperations] ;




    /*
    We decide if we take on the previous research
    */
    if ([self.patternSearched length] > 0
    &&
    [text rangeOfString:self.patternSearched].location != NSNotFound)
    {
    // We go on the previous research
    }
    else
    {
    // we start a new research

    self.filteredObjects = [NSMutableArray new] ;
    self.lastIndexTested = - 1 ;
    }


    /*
    We launch the new research
    */
    self.patternSearched = text ;

    self.researchQueue = [[NSOperationQueue alloc] init] ;
    NSOperation * researchOperation ;
    researchOperation = [NSBlockOperation blockOperationWithBlock:^{
    [self filterFromIndex:self.lastIndexTested + 1
    withText:text] ;
    }] ;
    [self.researchQueue addOperation:researchOperation] ;

    }





    - (void)filterWithTextFromIndex:(NSUInteger)startIndex
    withText:(NSString *)text
    {
    /*
    First, we filter the already filtered objects
    */
    NSArray * alreadySearched = [self.filteredObjects copy] ;

    for (NSString * label in alreadySearched)
    {
    if (![self doesPattern:text
    matchInLabel:label])
    {
    [self.filteredObjects removeObject:label] ;
    }
    }


    /*
    We continue the search
    */
    for (NSUInteger index = startIndex ; index < [self.datasource numberOfObjects] ; index++)
    {
    NSString * label = [self.datasource labelForObjectAtIndex:index] ;
    if ([self doesPattern:text
    matchInLabel:label])
    {
    [self.filteredObjects addObject:label] ;
    self.lastIndexTested = index ;
    }
    }


    /*
    We display the result
    */
    dispatch_async(dispatch_get_main_queue(),
    ^{
    [self.tableView reloadData] ;
    [self uncollapseTheTableView] ;
    }) ;
    }
  • FKDEVFKDEV Membre
    juin 2015 modifié #11

    Ces lignes là  peuvent faire perdre pas mal de temps en fonction du nombre d'objets déjà  triés :



    NSArray * alreadySearched = [self.filteredObjects copy] ;

    for (NSString * label in alreadySearched)
    {
    if (![self doesPattern:text
    matchInLabel:label])
    {
    [self.filteredObjects removeObject:label] ;
    }
    }


    Tu peux travailler directement sur ton tableau filteredObjects et noter les index à  retirer dans un NSIndexSet pour ensuite les virer tous d'un coup et bénéficier peut-être d'un code optimisé par Apple dans :



    -(void) removeObjectsAtIndexes:(NSIndexSet *)indexes;

    Et en tous cas éviter le removeObject qui va tout reparcourir depuis le début et faire isEqual pour chaque objet.


     


    Après tu pourrais aussi te passer complètement du tableau filteredObjects et utiliser un NSIndexSet à  la place pour référencer les objets du tableau principal qui correspondent à  la recherche, mais je ne suis pas certain que ça vaille le coup, puisque tu utilises déjà  des pointeurs.


     


     


    Sinon, t'as pas des problèmes d'accès concurrents à  ton tableau de résultats, là  ? Tu le remplis dans une queue de background et tu y accèdes dans la main queue pendant qu'une autre requête peut y accéder. Je pense que tu dois avoir au moins un @synchronize quelque part, sinon tu as potentiellement un problème.


  • AliGatorAliGator Membre, Modérateur

    Et en tous cas éviter le removeObject qui va tout reparcourir depuis le début et faire isEqual pour chaque objet.

    +1
     

    Sinon, t'as pas des problèmes d'accès concurrents à  ton tableau de résultats, là  ? Tu le remplis dans une queue de background et tu y accèdes dans la main queue pendant qu'une autre requête peut y accéder.

    C'est tout le problème du "code stateful" que j'évoquais plus haut, en effet. Il utilise des méthodes qui modifient en live ses propriétés (sans les protéger par un Lock/Mutex), donc gros problèmes d'accès concurrents en perspective, lecture d'un côté pendant que l'autre modifie.
    Alors que s'il faisait des méthodes stateless, donc qui ne modifient pas la propriété filteredObjects directement "sur place" dans la méthode, mais retourne un nouveau NSArray des objets filtrés (ou un NSIndexSet, ou autre), il n'y a plus ce risque, puisque la génération du nouveau NSArray ne contenant que les résultats filtrés pourra se faire en background sans affecter filteredObjects, et que filteredObjects ne sera modifié que par le main thread quand il fera "self.filteredObjects = [self filteredResultsWithText:...]" et pas par des tâches en background. Stateless powa.
  • FKDEVFKDEV Membre
    juin 2015 modifié #13

    Une alternative à  la proposition de Ali pour  "-performSelector:withObject:afterDelay:", est d'utiliser les dispatch_sources.


    C'est plus moderne mais moins facile à  comprendre et pas forcément plus efficace (à  voir). C'est pas YAGNI mais tu vas apprendre quelque chose.



    -(void)startSearch
    {
    dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_OR, 0, 0,
    dispatch_get_main_queue());
    dispatch_source_set_event_handler(source, ^{
    //block pour faire la recherche
    });
    dispatch_resume(source);
    }

    - (void)textFieldDidChange:(NSNotification*)aNotification
    {
    //.... noter le text à  chercher

    //prévenir que quelque chose a changé.
    dispatch_source_merge_data(source, 1);
    }

    Ton block de recherche sera appelée dès que possible sur la main queue (ou sur une autre queue si c'est préférable dans ton cas),


    dès qu'un caractère a été tapé. Mais si plusieurs caractères sont tapés avant que ton block ne soit exécuté. Il ne sera appelé qu'une seule fois.


     


    Par contre il faut mettre en place un mécanisme pour arrêter les blocks précédent. Ce qui n'est pas fait non plus dans ton code car ton cancellAllOperations ne sert à  rien si tu ne testes pas [NSOperation isCancelled], ce qui ne semble pas être fait vu que tu ne gardes pas de pointeur vers ton NSOperation.


  • Sinon l'idée de repartir de là  ou tu étais est bonne, en tous cas, j'ai pas trouvé de failles...




  • Sinon, t'as pas des problèmes d'accès concurrents à  ton tableau de résultats, là  ? Tu le remplis dans une queue de background et tu y accèdes dans la main queue pendant qu'une autre requête peut y accéder. Je pense que tu dois avoir au moins un @synchronize quelque part, sinon tu as potentiellement un problème.




     


    Je ne suis pas sûr, dis-moi ce que tu en penses :


     


    1) je commence à  remplir self.filteredObjects


    2) si une nouvelle recherche est lancée, la précédente recherche est stoppée (peut-être qu'il faudrait que j'ajoute un waitForEndOfTheOperationQueue...)


    3) on récupère éventuellement les anciens résultats et on relance une recherche


    4) à  la fin de la recherche, on appelle reloadData


     


    Je n'ai pas l'impression d'écrire à  plusieurs dans self.filteredObjects...


     


    Je n'ai encore jamais utilisé @synchro. Peut-être le moment de m'y mettre !

  • colas_colas_ Membre
    juin 2015 modifié #16

    Je vais tenir compte de vos remarques et connecter tableView à  une copie de self.filteredObjects


     


    Merci de vos remarques très instructives !


  • AliGatorAliGator Membre, Modérateur
    J'avoue avoir la flemme de lire le code mais quand je lis "repartir de là  où tu étais rendu", qu'est ce qui se passe si on tape "abc" puis qu'on tape la touche delete pour effacer le "c" ?
  • FKDEVFKDEV Membre
    juin 2015 modifié #18


    Je ne suis pas sûr, dis-moi ce que tu en penses :


     


    1) je commence à  remplir self.filteredObjects


    2) si une nouvelle recherche est lancée, la précédente recherche est stoppée (peut-être qu'il faudrait que j'ajoute un waitForEndOfTheOperationQueue...)


    3) on récupère éventuellement les anciens résultats et on relance une recherche


    4) à  la fin de la recherche, on appelle reloadData


     


    Je n'ai pas l'impression d'écrire à  plusieurs dans self.filteredObjects...


     


    Je n'ai encore jamais utilisé @synchro. Peut-être le moment de m'y mettre !




     


    2) non car cancelAllOperations n'arrête pas les opérations mais leur signale juste qu'elles sont annulées via leur propriété isCancel. Du coup oui il faudrait checker isCancel et mettre un système pour attendre la fin (ou passer en stateless). Et pas la peine de récréer une queue, tu peux juste recréer une NSOperation.


     


    On ne voit pas le code qui affiche les résultats, mais si ce code travaille directement sur self.filteredObjects, c'est encore un autre accès concurrent.


    Il faudrait soit travailler sur une copie, soit protéger tous les accès à  filteredObjects par des synchronized.


     


    Si tu travailles sur une copie, il faut protéger le moment de la copie par une propriété atomic ou par un synchronized:



    //quand la recherche est terminée... (ou de temps en temps si tu veux que ça bouge), updater le tableau à  afficher :

    @synchronized(self) //ou bien utiliser une property atomic pour resultToBedisplayed
    //perso je prefere voir des @synchronized.
    {
    self.resultToBeDisplayed = [self.filteredObjects copy];
    }
    [self.tableView reloadData];

    -(void)tableView:(UITableView*)tbView numberOfItemsInSection:(...)
    {
    return self.resultToBeDisplayed.count;
    //return self.filteredObjects.count; //pas bon...
    }



  • J'avoue avoir la flemme de lire le code mais quand je lis "repartir de là  où tu étais rendu", qu'est ce qui se passe si on tape "abc" puis qu'on tape la touche delete pour effacer le "c" ?




     


    Il reprend la recherche à  zéro.

  • AliGatorAliGator Membre, Modérateur
    Franchement passe en stateless tu auras beaucoup moins de soucis dans ton code.
  • colas_colas_ Membre
    juin 2015 modifié #21

    Tu veux dire :


     


    - je garde la fonctionnalité de reprise de la recherche précédente


    - mais je découple le datasource de la recherche 


     


    OU


     


    -  je supprime la fonctionnalité de reprise de la recherche précédente. Pour moi, cette fonctionnalité est par nature statefull.


     


     


    ?


  • AliGatorAliGator Membre, Modérateur
    Ce n'est que mon avis, mais je pense qu'il est préférable de faire un code stateless et ensuite de voir à  l'optimiser (faire du tout-stateless sans reprise de recherche, et voir ensuite si ça vaut vraiment le coup de faire le mécanisme de reprise ou pas) que de partir sur du stateful.

    Après, si la recherche est lente :
    - Soit il y a moyen de l'optimiser, je ne sais pas comment tu l'as codée mais par exemple si tu as une base CoreData, ça peut être intéressant d'activer le mode "indexé" du champ sur lequel tu recherches, pour accélérer fortement les requêtes, et voir aussi quel prédicat tu utilises.
    - Soit améliorer l'algo en soit
    - Si tu finis par conclure que tu as besoin du mécanisme de reprise de la recherche précédente, effectivement il est stateful par nature, mais tu peux l'isoler dans une classe si besoin et surtout faire en sorte que les modifications d'état soient le plus minimales possibles, en particulier éviter d'avoir comme propriété représentant l'état un NSMutableArray que tu viens modifier en live au fur et à  mesure de ton algo de recherche, mais plutôt que ta property lastFoundResults soit un NSArray, et que quand tu commences ton algo tu fasses une copy (ou mutableCopy si besoin) dessus, le filtre, et seulement à  la fin que tu affectes sa copy à  la propriété.

    En Objective-C, on va donc plutôt utiliser filteredArrayWithPredicate par exemple, qui prend un NSArray et t'en retourne un nouveau, plutôt que d'utiliser un NSMutableArray et appeler filterUsingPredicate dessus.

    En programmation fonctionnelle (ou le stateless est roi), on va plutôt utiliser des fonctions comme map ou reduce, qui ont un peu la même idée, à  savoir retourner un nouvel objet plutôt que de modifier l'objet d'origine, (et ne pas te donner accès à  l'éventuel objet mutable intermédiaire qui aura servi pendant l'alto de construction)

    En bref, ne pas faire :

    @property NSMutableArray* filteredResults;
    ...
    -(void)filterWithText:(NSString*)text {
    for ... {
    [self.filteredResults removeObject:...]; // tu modifies directement l'objet, pas bon
    }
    }
    Ni même ceci qui est un peu mieux mais pas encore parfait :

    @property NSMutableArray* filteredResults;
    ...
    -(void)filterWithText:(NSString*)text {
    NSMutableArray* results = [self.filteredResults mutableCopy];
    for ... in results {
    [results removeObject:...]; // tu modifies directement l'objet, pas bon
    // (En plus, modifier l'objet sur lequel on itère est une garantie de problèmes)
    }
    return results;
    }
    Mais plutôt :

    @property NSArray* filteredResults;
    ...
    -(NSArray)filteredResultsWithText:(NSString*)text {
    NSMutableArray* results = [NSMutableArray new];
    // Toujours mieux d'itérer sur une copie au cas où la propriété changerait pendant l'itération
    NSArray* lastResults = [self.filteredResults copy];
    for x in lastResults {
    // Si ça match, on ajoute
    [results addObject:x];
    }
    return [results copy]; // Retourner une copie non mutable
    }
    Ce ne sont que des exemples d'implémentation, je ne dis pas que c'est LA solution, mais c'est pout te donner une idée. Préférer les NSArray non-mutable surtout pour ceux que tu mets en propriété, et travailler sur des copies lors des itérations. Surtout qu'avec une signature comme filteredResultsWithText qui fait le traitement sur une copie et *retourne* le résultat, si tu exécutes tout ça sur un thread en background ou une @property, vu que rien dans la méthode ne change l'état de ton dataSource mais te retourne juste le nouveau tableau, c'est pas grave que tout ça se fasse en background il n'y a pas de risque de modifier ton objet dans un thread pendant qu'un autre l'utilise. Si ces méthodes sont faites dans un thread séparé c'est pas grave, elles font une copie de ton NSArray au début, donc partent du tableau pré-filtré à  l'instant T, et ensuite elles font tout dans leur coin en indépendant sans se soucier de l'état de l'objet (travaillant sur la copie du NSArray). Et le seul moment où tu changes l'état de l'objet finalement c'est au *retour* de la méthode, c'est l'appelant de "filteredResultsWithText:" qui va récupérer ce nouveau tableau et là  il pourra alors faire l'affectation de "self.filteredResults = le résultat obtenu", mais il fera certainement cette affectation dans le main thread, ce qui fait que seul le main thread modifiera l'état de ton objet, pas de risque que plusieurs le changent en même temps du coup. Et les threads secondaires faisant les filtrages en parallèle ne travailleront que sur une copie qu'ils auront fait au moment où ils auront démarré, sans risqué d'être ensuite perturbés si l'objet a changé pendant leur traitement, donc ça élimine tout un tas de soucis aussi.

    (Andy Matuschak a fait un talk très intéressant sur le sujet expliquant pourquoi le stateful est dangereux, principalement parce qu'on peut modifier l'état de l'objet de l'extérieur et avoir des effets de bord, ne pas être notifié qu'il a changé, etc. mais bon c'est orienté Swift et comment les "struct" et les "let" de Swift peuvent nous aider à  mieux faire du stateless)


    Au final, même en gardant ton mécanisme de "reprise de la recherche précédente" tu peux faire le *code* de ce filtrage en stateless. Seul le main thread modifiera le tableau des derniers résultats trouvés et sera stateful, mais si c'est concentré uniquement sur le main thread, c'est 100x plus facile à  contrôler.


  • (Andy Matuschak a fait un talk très intéressant sur le sujet expliquant pourquoi le stateful est dangereux, principalement parce qu'on peut modifier l'état de l'objet de l'extérieur et avoir des effets de bord, ne pas être notifié qu'il a changé, etc. mais bon c'est orienté Swift et comment les "struct" et les "let" de Swift peuvent nous aider à  mieux faire du stateless)




     


    Toujours intéressant tes posts Ali mais comme dirait Wikipedia : Références nécessaires (sur le talk de Andy Matuschak)

  • AliGatorAliGator Membre, Modérateur
    Je l'avais déjà  cité dans d'autres posts c'est pour ça.


    https://realm.io/news/andy-matuschak-controlling-complexity/
Connectez-vous ou Inscrivez-vous pour répondre.