2017-05-19 1 views
1

Remarque: J'ai recherché Stack Overflow pour des problèmes similaires, et aucune des questions que j'ai trouvées ne semble résoudre ce problème particulier.La méthode NSTableView reloadData provoque la mise à jour de NSProgressIndicator dans toutes les lignes et le scintillement

J'ai écrit une petite application de l'échantillon (le projet Xcode avec le code source disponible ici: http://jollyroger.kicks-ass.org/stackoverflow/FlickeringTableView.zip) qui joue tous les sons en /System/Library/Sounds/ séquentielle et affiche les sons dans une fenêtre comme ils sont joués pour montrer la question que je je vois. La fenêtre de MainMenu.xib a une seule colonne NSTableView avec une ligne définie comme un modèle de cellule avec trois éléments qu'elle contient:

  • un NSTextField pour contenir le nom du son
  • autre NSTextField pour contenir les informations sonores
  • un NSProgressIndicator pour montrer les progrès de jeu alors que le son joue

Je NSTableCellView sous-classé (SoundsTableCellView.h) pour définir chacun des éléments dans la cellule Vie w afin que je puisse accéder et les définir quand le moment se présente.

J'ai défini une classe MySound qui encapsule les propriétés et les méthodes nécessaires pour gérer la lecture des fichiers audio via les API AVAudioPlayer. Cette classe définit un protocole MySoundDelegate pour permettre au délégué de l'application de recevoir des événements à chaque fois que les sons commencent ou se terminent.

Le délégué de l'application est conforme aux NSTableViewDelegate et NSTableViewDataSource protocoles pour permettre de stocker les données de la table comme un tableau de MySound objets et mettre à jour le tableau des informations pertinentes en cas de besoin. Il adhère également au protocole MySoundDelegate pour recevoir des événements lorsque les sons commencent ou finissent de jouer. Le délégué a également une tâche NSTimer qui appelle périodiquement une méthode refreshWindow pour mettre à jour l'indicateur de progression du son en cours de lecture. La méthode refreshWindow du délégué de l'application affiche et redimensionne la fenêtre si nécessaire en fonction du nombre de sons de la liste, et met à jour la référence stockée au NSProgressIndicator associé au son en cours de lecture.

La méthode tableView: viewForTableColumn (NSTableViewDelegate) du délégué de l'application est appelée pour remplir les cellules du tableau.Dans ce document, j'utilise la norme d'Apple « Remplir une table Voir Programmatically » conseil à:

  1. vérifier l'identifiant de colonne de table pour assurer qu'elle correspond à l'identificateur (sound column) Je réglerai Interface Builder (Xcode) pour la colonne de table ,
  2. obtenir la cellule du tableau correspondant à l'identificateur (sound cell) en appelant thisTableView makeViewWithIdentifier,
  3. utiliser le paramètre row entrant pour localiser l'élément de tableau de correspondance de la source de données (matrice de délégué d'application sounds), puis
  4. définir les valeurs de chaînes de NSTextFields et régler la maxValue et doubleValue du NSProgressIndicator dans la cellule pour les détails correspondants de l'objet sonore associé,
  5. stocker une référence à la commande NSProgressIndicator associée à l'objet audio associée pour la mise à jour plus tard

est ici la méthode :

- (NSView *)tableView:(NSTableView *)thisTableView viewForTableColumn:(NSTableColumn *)thisTableColumn row:(NSInteger)thisRow 
{ 
    SoundsTableCellView *cellView = nil; 

    // get the table column identifier 

    NSString *columnID = [thisTableColumn identifier]; 
    if ([columnID isEqualToString:@"sound column"]) 
    { 
     // get the sound corresponding to the specified row (sounds array index) 

     MySound *sound = [sounds objectAtIndex:thisRow]; 

     // get an existing cell from IB with our hard-coded identifier 

     cellView = [thisTableView makeViewWithIdentifier:@"sound cell" owner:self]; 

     // display sound name 

     [cellView.soundName setStringValue:[sound name]]; 
     [cellView.soundName setLineBreakMode:NSLineBreakByTruncatingMiddle]; 

     // display sound details (source URL) 

     NSString *details = [NSString stringWithFormat:@"%@", [sound sourceURL]]; 
     [cellView.soundDetails setStringValue:details]; 
     [cellView.soundDetails setLineBreakMode:NSLineBreakByTruncatingMiddle]; 

     // update progress indicators 

     switch ([sound state]) 
     { 
      case kMySoundStateQueued: 
       break; 
      case kMySoundStateReadyToPlay: 
       break; 
      case kMySoundStatePlaying: 
       if (sound.playProgress == nil) 
       { 
        sound.playProgress = cellView.playProgress; 
       } 

       NSTimeInterval duration = [sound duration]; 
       NSTimeInterval position = [sound position]; 

       NSLog(@"row %ld: %@ (%f/%f)", (long)thisRow, [sound name], position, duration); 
       NSLog(@"   %@: %@", [sound name], sound.playProgress); 

       [cellView.playProgress setMaxValue:duration]; 
       [cellView.playProgress setDoubleValue:position]; 

       break; 
      case kMySoundStatePaused: 
       break; 
      case kMySoundStateFinishedPlaying: 
       break; 
      default: 
       break; 
     } 
    } 

    return cellView; 
} 

Et voici la méthode refreshWindow:

- (void) refreshWindow 
{ 
    if ([sounds count] > 0) 
    { 
     // show window if needed 

     if ([window isVisible] == false) 
     { 
      [window makeKeyAndOrderFront:self]; 
     } 

     // resize window to fit all sounds in the list if needed 

     NSRect frame = [self.window frame]; 

     int screenHeight = self.window.screen.frame.size.height; 

     long maxRows = ((screenHeight - 22)/82) - 1; 
     long displayedRows = ([sounds count] > maxRows ? maxRows : [sounds count]); 

     long actualHeight = frame.size.height; 
     long desiredHeight = 22 + (82 * displayedRows); 
     long delta = desiredHeight - actualHeight; 

     if (delta != 0) 
     { 
      frame.size.height += delta; 
      frame.origin.y -= delta; 

      [self.window setFrame:frame display:YES]; 
     } 

     // update play position of progress indicator for all sounds in the list 

     for (MySound *nextSound in sounds) 
     { 
      switch ([nextSound state]) 
      { 
       case kMySoundStatePlaying: 
        if (nextSound.playProgress != nil) 
        { 
         [nextSound.playProgress setDoubleValue:[nextSound position]]; 
         NSLog(@"   %@: %@ position: %f", [nextSound name], nextSound.playProgress, [nextSound position]); 
        } 
        break; 
       case kMySoundStateQueued: 
       case kMySoundStateReadyToPlay: 
       case kMySoundStatePaused: 
       case kMySoundStateFinishedPlaying: 
       default: 
        break; 
      } 
     } 
    } 
    else 
    { 
     // hide window 

     if ([window isVisible]) 
     { 
      [window orderOut:self]; 
     } 
    } 

    // reload window table view 

    [tableView reloadData]; 
} 

Pendant init, le délégué de l'application scanne le dossier /System/Library/Sounds/ pour obtenir une liste des fichiers audio AIFF dans ce dossier, et crée un tableau sounds tenant des objets sonores pour chacun des sons dans ce dossier. La méthode applicationDidFinishLaunching démarre ensuite la lecture séquentielle du premier son de la liste. Le problème (que vous pouvez voir en exécutant l'exemple de projet) est qu'au lieu de mettre à jour uniquement la ligne de la table supérieure pour le son en cours de lecture, les indicateurs de progression tous les lignes semblent mettre à jour et scintillement aussi. La façon dont il est affiché est quelque peu incohérente (parfois ils clignotent tous, et parfois ils sont tous vierges comme prévu); mais quand ils se mettent à jour et scintillent, les indicateurs de progression semblent correspondre à peu près au son en cours de lecture. Je suis donc assez sûr que la question doit être en quelque sorte liée à la façon dont je suis mise à jour la table; Je ne suis pas sûr où le problème est ou comment le résoudre.

Voici une capture d'écran de ce que la fenêtre ressemble à vous donner une idée:

Table View Screen Shot

Toutes les idées ou conseils serait grandement apprécié!

+0

Pour votre information: documentation "Remplir une table Voir Programmatically" d'Apple est ici: https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/TableView/PopulatingView-TablesProgrammatically/PopulatingView-TablesProgrammatically.html –

+0

' makeViewWithIdentifier: owner: '" Retourne une vue nouvelle ou ** existante ** avec l'identifiant spécifié. ". Tous les sons peuvent pointer vers le même contrôle. – Willeke

+0

Merci, @Willeke. J'ai vérifié, et il apparaît que l'indicateur de progression est bien différent de chaque ligne. J'ai ajouté à l'instruction 'NSLog' dans ma méthode' makeViewWithIdentifier' pour sortir la référence stockée dans 'NSProgressIndicator' pour l'objet sonore donné. Voici quelques exemple de sortie: Basso [] Coup [] Bottle [] Frog [] –

Répondre

0

Voici ce que j'ai changé.

tableView:viewForTableColumn:row: renvoie «une vue pour afficher la ligne et la colonne spécifiées». La valeur de la barre de progression est toujours définie.

- (NSView *)tableView:(NSTableView *)thisTableView viewForTableColumn:(NSTableColumn *)thisTableColumn row:(NSInteger)thisRow 
{ 
    SoundsTableCellView *cellView = nil; 

    // get the table column identifier 

    NSString *columnID = [thisTableColumn identifier]; 
    if ([columnID isEqualToString:@"sound column"]) 
    { 
     // get the sound corresponding to the specified row (sounds array index) 

     MySound *sound = [sounds objectAtIndex:thisRow]; 

     // get an existing cell from IB with our hard-coded identifier 

     cellView = [thisTableView makeViewWithIdentifier:@"sound cell" owner:self]; 

     // display sound name 

     [cellView.soundName setStringValue:[sound name]]; 

     // display sound details (source URL) 

     NSString *details = [NSString stringWithFormat:@"%@", [sound sourceURL]]; 
     [cellView.soundDetails setStringValue:details]; 

     // update progress indicators 

     // [cellView.playProgress setUsesThreadedAnimation:NO]; 

     NSTimeInterval duration = [sound duration]; 
     NSTimeInterval position = [sound position]; 
     [cellView.playProgress setMaxValue:duration]; 
     [cellView.playProgress setDoubleValue:position]; 
    } 

    // end updates 

    // [thisTableView endUpdates]; 

    return cellView; 
} 

refreshWindow est divisé en refreshProgress et refreshWindow. refreshProgress rafraîchit la ligne du son en cours de lecture et appelle une minuterie.

- (void)refreshProgress 
{ 
    if ([sounds count] > 0) 
    { 
     [sounds enumerateObjectsUsingBlock:^(MySound *nextSound, NSUInteger rowNr, BOOL *stop) 
     { 
      switch ([nextSound state]) 
      { 
       case kMySoundStatePlaying: 
        // refresh row 
        [tableView reloadDataForRowIndexes:[NSIndexSet indexSetWithIndex:rowNr] 
         columnIndexes:[NSIndexSet indexSetWithIndex:0]]; 
        break; 
       case kMySoundStateQueued: 
       case kMySoundStateReadyToPlay: 
       case kMySoundStatePaused: 
       case kMySoundStateFinishedPlaying: 
       default: 
        break; 
      } 
     }]; 
    } 
} 

refreshWindow Actualise la taille et la visibilité de la fenêtre et est appelée lorsque le nombre de changements de sons.

- (void) refreshWindow 
{ 
    if ([sounds count] > 0) 
    { 
     // show window if needed 

     if ([window isVisible] == false) 
     { 
      [window makeKeyAndOrderFront:self]; 
     } 

     // resize window to fit all sounds in the list if needed 

     ... calculate new window frame 

     } 
    else 
    { 
     // hide window 

     if ([window isVisible]) 
     { 
      [window orderOut:self]; 
     } 
    } 
} 

Lorsqu'un son est enlevé, la ligne est également supprimée de sorte que les autres lignes affichent toujours le même son et ne nécessitent pas une mise à jour.

- (void) soundFinishedPlaying:(MySound *)sound encounteredError:(NSError *)error 
{ 
    if (error != NULL) 
    { 
     // display an error dialog box to the user 

     [NSApp presentError:error]; 
    } 
    else 
    { 
     // remove sound from array 

     NSLog(@"deleting: [%@|%@]", [sound truncatedID], [sound name]); 

     NSUInteger index = [sounds indexOfObject:sound]; 
     [sounds removeObject:sound]; 
     [tableView removeRowsAtIndexes:[NSIndexSet indexSetWithIndex:index] withAnimation:NSTableViewAnimationEffectNone]; 
    } 

    // refresh window 

    [self refreshWindow]; 

    // play the next sound in the queue 

    [self play]; 
} 

[tableView reloadData] n'est pas appelée. sound.playProgress n'est pas utilisé.

+0

Aha. Il semble donc que la réponse ne consiste pas à appeler '[tableView reloadData]' du tout, et à appeler à la place '[tableView reloadDataForRowIndexes ...]' à la place, avec '[tableView removeRowsAtIndexes ...]' pour supprimer manuellement les lignes supprimées éléments dans le tableau de source de données. Je ne comprends toujours pas complètement _why_ 'reloadData' provoque des indicateurs de progression _all_ dans la vue tableau pour mettre à jour/scintiller, mais il est bon qu'il existe une meilleure façon de les mettre à jour. Merci de m'avoir montré le chemin! :) –

+0

'makeViewWithIdentifier' réutilise les vues, lorsqu'une vue n'est plus utilisée, elle est placée dans un pool de vues réutilisables. Le scintillement commence lorsque le premier son est supprimé. Quand il y a 10 vues et 9 sons, chaque 'reloadData' les 9 premières vues sont réutilisées et la 10ème vue est utilisée en premier sur le' reloadData' suivant. Les vues circulent et 'viewForTableColumn' n'a pas défini la valeur de l'indicateur de progression dans chaque vue. Vous pouvez voir cela se produire si vous connectez 'sound.name' et le pointeur de' cellView.playProgress' dans 'viewForTableColumn'. – Willeke

+0

Je vois maintenant - je l'ai. Merci, @Willeke! :) –