web-dev-qa-db-fra.com

Gardez uitableview statique lors de l'insertion de lignes en haut

J'ai une tableView dans laquelle j'insère des lignes en haut.

Pendant que je fais cela, je veux que la vue actuelle reste complètement immobile, donc les lignes n'apparaissent que si vous faites défiler vers le haut.

J'ai essayé de sauvegarder la position actuelle de UIScrollview sous-jacent et de la réinitialiser après que les lignes aient été insérées, mais cela donne un tremblement, qui monte et descend, même si elle se retrouve au même endroit.

Y a-t-il un bon moyen d'y parvenir?

Mise à jour: J'utilise beginUpdate, puis insertRowsAtIndexPath, endUpdates. Il n'y a pas d'appel reloadData.

scrollToRowAtIndexPath saute au sommet de la cellule en cours (enregistré avant l'ajout de lignes).

L’autre approche que j’ai essayée et qui aboutit au exactement le bon rythme, mais avec un judder, c’est.

save tableView currentOffset. (Underlying scrollView method)
Add rows (beginUpdates,insert...,endUpdates) 
reloadData ( to force a recalulation of the scrollview size )
Recalculate the correct new offset from the bottom of the scrollview
setContentOffset (Underlying scrollview method)

Le problème est que reloadData provoque le défilement bref du scrollview/tableview, puis le setContentOffset le renvoie au bon endroit.

Existe-t-il un moyen de faire en sorte qu'une tableView calcule sa nouvelle taille sans lancer l'affichage?

Emballer le tout dans un débutAnimation commitAnimation n'aide pas beaucoup non plus.

Mise à jour 2: Cela peut être clairement réalisé - consultez l'application Twitter officielle pour une application lorsque vous recherchez des mises à jour.

60
Dean Smith

Il n'est vraiment pas nécessaire de résumer la hauteur de toutes les lignes, Le nouveau contentSize après le rechargement de la table représente déjà cela .. Il vous suffit donc de calculer le delta de la hauteur de contentSize et de l'ajouter au décalage actuel.

    ...
    CGSize beforeContentSize = self.tableView.contentSize;
    [self.tableView reloadData];
    CGSize afterContentSize = self.tableView.contentSize;

    CGPoint afterContentOffset = self.tableView.contentOffset;
    CGPoint newContentOffset = CGPointMake(afterContentOffset.x, afterContentOffset.y + afterContentSize.height - beforeContentSize.height);
    self.tableView.contentOffset = newContentOffset;
    ...
48
AmitP
-(void) updateTableWithNewRowCount : (int) rowCount
{    
    //Save the tableview content offset 
    CGPoint tableViewOffset = [self.tableView contentOffset];                                                                                                            

    //Turn of animations for the update block 
    //to get the effect of adding rows on top of TableView
    [UIView setAnimationsEnabled:NO];

    [self.tableView beginUpdates];                    

    NSMutableArray *rowsInsertIndexPath = [[NSMutableArray alloc] init];        

    int heightForNewRows = 0;

    for (NSInteger i = 0; i < rowCount; i++) {

        NSIndexPath *tempIndexPath = [NSIndexPath indexPathForRow:i inSection:SECTION_TO_INSERT];
        [rowsInsertIndexPath addObject:tempIndexPath];

        heightForNewRows = heightForNewRows + [self heightForCellAtIndexPath:tempIndexPath];                        
    }

    [self.tableView insertRowsAtIndexPaths:rowsInsertIndexPath withRowAnimation:UITableViewRowAnimationNone];                                                                            

    tableViewOffset.y += heightForNewRows;                                                                                 

    [self.tableView endUpdates]; 

    [UIView setAnimationsEnabled:YES];

    [self.tableView setContentOffset:tableViewOffset animated:NO];           
}


-(int) heightForCellAtIndexPath: (NSIndexPath *) indexPath
{

    UITableViewCell *cell =  [self.tableView cellForRowAtIndexPath:indexPath];

    int cellHeight   =  cell.frame.size.height;

    return cellHeight;
}

Passez simplement le nombre de lignes des nouvelles lignes à insérer en haut. 

24
Mayank Yadav

La manière dont Dean utilise un cache d'image est trop compliquée et je pense que cela détruit la réactivité de l'interface utilisateur.

Une méthode appropriée: Utilisez une sous-classe UITableView et substituez -setContentSize: dans lequel vous pouvez, par certains moyens, calculer l’ajustement de la vue tabulaire et le compenser en définissant contentOffset .

Il s'agit d'un exemple de code simple permettant de gérer la situation la plus simple dans laquelle toutes les insertions ont lieu en haut de la vue du tableau:

@implementation MyTableView

- (void)setContentSize:(CGSize)contentSize {
        // I don't want move the table view during its initial loading of content.
    if (!CGSizeEqualToSize(self.contentSize, CGSizeZero)) {
        if (contentSize.height > self.contentSize.height) {
            CGPoint offset = self.contentOffset;
            offset.y += (contentSize.height - self.contentSize.height);
            self.contentOffset = offset;
        }
    }
    [super setContentSize:contentSize];
}

@end
21
an0

eu le même problème et trouvé une solution. 

    save tableView currentOffset. (Underlying scrollView method)
    //Add rows (beginUpdates,insert...,endUpdates) // don't do this!
    reloadData ( to force a recalulation of the scrollview size )
    add newly inserted row heights to contentOffset.y here, using tableView:heightForRowAtIndexPath:
    setContentOffset (Underlying scrollview method)

comme ça:

- (CGFloat) firstRowHeight
{
    return [self tableView:[self tableView] heightForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];
}

...
CGPoint offset = [[self tableView] contentOffset];
[self tableView] reloadData];
offset.y += [self firstRowHeight];
if (offset.y > [[self tableView] contentSize].height) {
    offset.y = 0;
}
[[self tableView] setContentOffset:offset];
...

fonctionne parfaitement, sans problèmes. 

17
Andrey Zverev

J'ai effectué des tests avec un projet d'échantillon de données de base et je l'ai laissé immobile pendant que de nouvelles cellules étaient ajoutées au-dessus de la cellule visible supérieure. Ce code nécessiterait un ajustement pour les tables avec un espace vide à l'écran, mais une fois l'écran rempli, cela fonctionnera correctement.

static CGPoint  delayOffset = {0.0};

- (void)controllerWillChangeContent:(NSFetchedResultsController*)controller {
    if ( animateChanges )
        [self.tableView beginUpdates];
    delayOffset = self.tableView.contentOffset; // get the current scroll setting
}

Ajouté ceci aux points d'insertion de cellules. Vous pouvez effectuer une soustraction de contrepartie pour la suppression de cellules.

    case NSFetchedResultsChangeInsert:

        delayOffset.y += self.tableView.rowHeight;  // add for each new row
        if ( animateChanges )   
            [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationNone];
        break;

et enfin

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
    if ( animateChanges )   
    {
        [self.tableView setContentOffset:delayOffset animated:YES];
        [self.tableView endUpdates];
    }
    else
    {
        [self.tableView reloadData];
        [self.tableView setContentOffset:delayOffset animated:NO];
    }
}

Avec animateChanges = NO, je ne pouvais rien voir bouger lorsque des cellules étaient ajoutées.

Lors des tests avec animateChanges = YES, le "judder" était présent. Il semble que l'animation d'insertion de cellule n'avait pas la même vitesse que le défilement de table animé. Bien que le résultat final puisse se terminer par des cellules visibles exactement à leur point de départ, le tableau tout entier semble se déplacer de 2 ou 3 pixels, puis reculer. 

Si les vitesses d'animation peuvent être égales, elle peut sembler rester en place. 

Cependant, lorsque j'appuyais sur le bouton pour ajouter des lignes avant la fin de l'animation précédente, l'animation s'arrêtait brusquement et la suivante commençait, ce qui entraînait un changement de position brutal.

9
Walt Sellers

@Doyen,

Vous pouvez modifier votre code comme ceci pour empêcher l'animation.

[tableView beginUpdates];
[UIView setAnimationsEnabled:NO];

// ...

[tableView endUpdates];

[tableView setContentOffset:newOffset animated:NO];
[UIView setAnimationsEnabled:YES];
5
Norris Tong

Tout le monde aime copier et coller des exemples de code, voici donc une implémentation de la réponse d'Andrey Z.

Ceci est dans ma méthode delegateDidFinishUpdating:(MyDataSourceDelegate*)delegate

if (self.contentOffset.y <= 0)
{
    [self beginUpdates];
    [self insertRowsAtIndexPaths:insertedIndexPaths withRowAnimation:insertAnimation];
    [self endUpdates];
}
else
{
    CGPoint newContentOffset = self.contentOffset;
    [self reloadData];

    for (NSIndexPath *indexPath in insertedIndexPaths)
        newContentOffset.y += [self.delegate tableView:self heightForRowAtIndexPath:indexPath];

    [self setContentOffset:newContentOffset];

    NSLog(@"New data at top of table view");
}

La NSLog en bas peut être remplacée par un appel pour afficher une vue indiquant que de nouvelles données sont disponibles.

4
mikezs

J'ai été confronté à une situation dans laquelle de nombreuses sections peuvent avoir un nombre de lignes différent entre les appels -reloadData en raison d'un regroupement personnalisé et où la hauteur des lignes varie. Donc, voici une solution basée sur AndreyZ. Il contentHeight propriété de UIScrollView avant et après -reloadData et il semble plus universel.

CGFloat contentHeight = self.tableView.contentSize.height;
CGPoint offset = self.tableView.contentOffset;
[self.tableView reloadData];

offset.y += (self.tableView.contentSize.height - contentHeight);
if (offset.y > [self.tableView contentSize].height)
    offset.y = 0;

[self.tableView setContentOffset:offset];
3

Je veux ajouter une condition supplémentaire . Si votre code dans iOS11 ou plus, vous devez faire comme ci-dessous;

Dans iOS 11, les vues de table utilisent les hauteurs estimées par défaut. Cela signifie que la taille du contenu est juste comme valeur estimée initialement. Si vous devez utiliser contentSize, vous voudrez désactiver les hauteurs estimées en définissant les 3 propriétés de hauteur estimée à zéro: 

tableView.estimatedRowHeight = 0 tableView.estimatedSectionHeaderHeight = 0 tableView.estimatedSectionFooterHeight = 0

2
pusswzy

Comment ajoutez-vous les lignes à la table?

Si vous modifiez la source de données et appelez ensuite reloadData, la table risque alors de faire défiler à nouveau la table.

Toutefois, si vous utilisez les méthodes beginUpdates, insertRowsAtIndexPaths:withRowAnimation:, endUpdates, vous devriez pouvoir insérer des lignes sans avoir à appeler reloadData et ainsi conserver la table dans sa position d'origine.

N'oubliez pas de modifier votre source de données avant d'appeler endUpdates, faute de quoi vous obtiendrez une exception d'incohérence interne.

1
Jasarien

Cela ne semble pas possible si vous renvoyez des hauteurs estimées pour la vue de table.

   - (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath ;

Si vous implémentez cette méthode et que vous retournez une hauteur approximative, votre vue table sautera lors du rechargement, car il semble utiliser ces hauteurs lors de la définition des décalages.

Pour que cela fonctionne, utilisez l’une des réponses ci-dessus (j’y suis allé avec la réponse @Mayank Yadav), n’implémentez pas la méthode EstimationHeight et mettez en cache la hauteur des cellules (n'oubliez pas de régler le cache lorsque vous insérez d'autres cellules en haut).

1
bencallis

Solution simple pour désactiver les animations

func addNewRows(indexPaths: [NSIndexPath]) {
    let addBlock = { () -> Void in
        self.tableView.beginUpdates()
        self.tableView.insertRowsAtIndexPaths(indexPaths, withRowAnimation: .None)
        self.tableView.endUpdates()
    }

    tableView.contentOffset.y >= tableView.height() ? UIView.performWithoutAnimation(addBlock) : addBlock()
}
1
Sahil Kapoor

J'ai finalement résolu le problème en convertissant la vue de table actuelle en UIImage, puis en plaçant une vue temporaire UIImageView sur la vue de table pendant son animation.

Le code suivant va générer l'image


// Save the current tableView as an UIImage
CSize pageSize = [[self tableView] frame].size;
UIGraphicsBeginImageContextWithOptions(pageSize, YES, 0.0); // 0.0 means scale appropriate for device ( retina or no )
CGContextRef resizedContext = UIGraphicsGetCurrentContext();
CGPoint offset = [[self tableView] contentOffset];
CGContextTranslateCTM(resizedContext,-(offset.x),-(offset.y));

[[[self tableView  ]layer] renderInContext:resizedContext];
UIImage *viewImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();    

Vous devez garder une trace de la croissance de la vue table lors de l'insertion de lignes et vous assurer de faire défiler la vue table exactement au même endroit.

1
Dean Smith

Vous n'avez pas besoin de faire autant d'opérations difficiles, de plus, ces manipulations ne fonctionneraient pas parfaitement. La solution simple consiste à faire pivoter la vue tableau, puis à y faire pivoter les cellules.

tableView.transform = CGAffineTransformMakeRotation(M_PI);

-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 
{

   cell.transform = CGAffineTransformMakeRotation(M_PI);

}

Utilisez [tableView setScrollIndicatorInsets:UIEdgeInsetsMake(0, 0, 0, 310)] pour définir la position relative de l'indicateur de défilement. Ce sera sur le côté droit après la rotation de la vue de la table.

1
Nikolay Tabunchenko

En fin de soirée, cela fonctionne même lorsque les cellules ont des hauteurs dynamiques (alias UITableViewAutomaticDimension). Il n'est pas nécessaire de parcourir les cellules pour calculer leur taille. un peu de math, il est probablement possible d'adapter cela à chaque situation:

func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
        if indexPath.row == 0 {
            self.getMoreMessages()
        }
}

private func getMoreMessages(){
        var initialOffset = self.tableView.contentOffset.y
        self.tableView.reloadData()
        //@numberOfCellsAdded: number of items added at top of the table 
        self.tableView.scrollToRowAtIndexPath(NSIndexPath(forRow: numberOfCellsAdded, inSection: 0), atScrollPosition: .Top, animated: false)
        self.tableView.contentOffset.y += initialOffset
}
0
ZekeMidas

D'après la réponse d'Andrey Z, voici un exemple concret qui fonctionne parfaitement pour moi ...

int numberOfRowsBeforeUpdate = [controller.tableView numberOfRowsInSection:0];
CGPoint currentOffset = controller.tableView.contentOffset;

if(numberOfRowsBeforeUpdate>0)
{
    [controller.tableView reloadData];
    int numberOfRowsAfterUpdate = [controller.tableView numberOfRowsInSection:0];

    float rowHeight = [controller getTableViewCellHeight];  //custom method in my controller
    float offset = (numberOfRowsAfterUpdate-numberOfRowsBeforeUpdate)*rowHeight;

    if(offset>0)
    {
        currentOffset.y = currentOffset.y+offset;
        [controller.tableView setContentOffset:currentOffset];
    }
}
else
    [controller.tableView reloadData];
0
boostedz06

Réponses AmitP, version Swift 3

let beforeContentSize = self.tableView.contentSize
self.tableView.reloadData()
let afterContentSize = self.tableView.contentSize
let afterContentOffset = self.tableView.contentOffset
let newContentOffset = CGPoint(x: afterContentOffset.x, y: afterContentOffset.y + afterContentSize.height - beforeContentSize.height)
                    self.tableView.contentOffset = newContentOffset
0
ddnl