web-dev-qa-db-fra.com

Mise en page UITableView gâcher sur Push segue et de retour. (iOS 8, Xcode beta 5, Swift)

tldr; Les contraintes automatiques semblent s'interrompre sous Push segue et revenir à l'affichage pour les cellules personnalisées

Edit: j'ai fourni un exemple de projet github qui montre l'erreur qui se produit https://github.com/Matthew-Kempson/TableViewExample.git

Je suis en train de créer une application qui nécessite l'étiquette de titre de la UITableCell personnalisée pour permettre des lignes variables en fonction de la longueur du titre de l'article. Les cellules se chargent correctement dans la vue mais si j'appuie sur une cellule pour charger la publication dans une séquence contenant une vue contenant un WKWebView, vous pouvez voir que, comme le montre la capture d'écran, les cellules se déplacent immédiatement vers des positions incorrectes. Ceci est également affiché lors du chargement de la vue via le bouton de retour du contrôleur UINavigationController.

Dans cet exemple particulier, j'ai appuyé sur la dernière cellule, avec le titre "Deux copains dont j'ai pris une photo à Paris", et tout est chargé correctement. Ensuite, comme indiqué dans la capture d'écran suivante, les cellules se déplacent toutes vers le haut pour des raisons inconnues lors du chargement de la deuxième vue. Ensuite, lorsque je réaffiche la vue, vous pouvez voir que l’écran s’est légèrement décalé vers le haut et que je ne peux pas réellement faire défiler plus bas que ce qui est indiqué. Cela semble être aléatoire comme avec d'autres tests lorsque la vue est chargée, il y a un espace blanc sous la cellule du bas qui ne disparaît pas.

J'ai également inclus une image contenant les contraintes des cellules.

Images (j'ai besoin de plus de réputation pour fournir des images dans cette question apparemment, donc elles sont dans cet album imgur): http://imgur.com/a/gY87E

Mon code:

Méthode dans la cellule personnalisée pour permettre à la cellule de redimensionner correctement la vue lors d'une rotation:

override func layoutSubviews() {
    super.layoutSubviews()

    self.contentView.layoutIfNeeded()

    // Update the label constaints
    self.titleLabel.preferredMaxLayoutWidth = self.titleLabel.frame.width
    self.detailsLabel.preferredMaxLayoutWidth = self.detailsLabel.frame.width
}

Code dans la table

override func viewDidLoad() {
    super.viewDidLoad()

    // Create and register the custom cell
    self.tableView.estimatedRowHeight = 56
    self.tableView.rowHeight = UITableViewAutomaticDimension
}

Code pour créer la cellule

    override func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell! {
    if let cell = tableView.dequeueReusableCellWithIdentifier("LinkCell", forIndexPath: indexPath) as? LinkTableViewCell {

        // Retrieve the post and set details
        let link: Link = self.linksArray.objectAtIndex(indexPath.row) as Link

        cell.titleLabel.text = link.title
        cell.scoreLabel.text = "\(link.score)"
        cell.detailsLabel.text = link.stringCreatedTimeIntervalSinceNow() + " ago by " + link.author + " to /r/" + link.subreddit

        return cell
    }

    return nil
}

Si vous avez besoin de plus de code ou d'informations, veuillez demander et je fournirai ce qui est nécessaire

Merci de votre aide!

40
matthew.kempson

Le problème de ce comportement est que lorsque vous appuyez sur une séquence, la variable tableView appelle la variable estimatedHeightForRowAtIndexPath pour les cellules visibles et réinitialise la hauteur de la cellule à une valeur par défaut. Cela se produit après l'appel viewWillDisappear. Si vous revenez à TableView, toutes les cellules visibles sont gâchées.

J'ai résolu ce problème avec un estimatedCellHeightCache. J'ajoute simplement ce code extrait à la méthode cellForRowAtIndexPath:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    ...
    // put estimated cell height in cache if needed
    if (![self isEstimatedRowHeightInCache:indexPath]) {
        CGSize cellSize = [cell systemLayoutSizeFittingSize:CGSizeMake(self.view.frame.size.width, 0) withHorizontalFittingPriority:1000.0 verticalFittingPriority:50.0];
        [self putEstimatedCellHeightToCache:indexPath height:cellSize.height];
    }
    ...
}

Maintenant, vous devez implémenter la estimatedHeightForRowAtIndexPath comme suit:

-(CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
    return [self getEstimatedCellHeightFromCache:indexPath defaultHeight:41.5];
}

Configurer le cache

Ajoutez cette propriété à votre fichier .h:

@property NSMutableDictionary *estimatedRowHeightCache;

Implémentez des méthodes pour mettre/get/reset .. le cache:

#pragma mark - estimated height cache methods

// put height to cache
- (void) putEstimatedCellHeightToCache:(NSIndexPath *) indexPath height:(CGFloat) height {
    [self initEstimatedRowHeightCacheIfNeeded];
    [self.estimatedRowHeightCache setValue:[[NSNumber alloc] initWithFloat:height] forKey:[NSString stringWithFormat:@"%d", indexPath.row]];
}

// get height from cache
- (CGFloat) getEstimatedCellHeightFromCache:(NSIndexPath *) indexPath defaultHeight:(CGFloat) defaultHeight {
    [self initEstimatedRowHeightCacheIfNeeded];
    NSNumber *estimatedHeight = [self.estimatedRowHeightCache valueForKey:[NSString stringWithFormat:@"%d", indexPath.row]];
    if (estimatedHeight != nil) {
        //NSLog(@"cached: %f", [estimatedHeight floatValue]);
        return [estimatedHeight floatValue];
    }
    //NSLog(@"not cached: %f", defaultHeight);
    return defaultHeight;
}

// check if height is on cache
- (BOOL) isEstimatedRowHeightInCache:(NSIndexPath *) indexPath {
    if ([self getEstimatedCellHeightFromCache:indexPath defaultHeight:0] > 0) {
        return YES;
    }
    return NO;
}

// init cache
-(void) initEstimatedRowHeightCacheIfNeeded {
    if (self.estimatedRowHeightCache == nil) {
        self.estimatedRowHeightCache = [[NSMutableDictionary alloc] init];
    }
}

// custom [self.tableView reloadData]
-(void) tableViewReloadData {
    // clear cache on reload
    self.estimatedRowHeightCache = [[NSMutableDictionary alloc] init];
    [self.tableView reloadData];
}
27
Kai Burghardt

Ce bogue est dû à l'absence de méthode tableView:estimatedHeightForRowAtIndexPath:. C'est une partie optionnelle du protocole UITableViewDelegate.

Ce n'est pas comme ça que ça doit fonctionner. La documentation d'Apple indique:

Fournir une estimation de la hauteur des lignes peut améliorer l'expérience de l'utilisateur lors du chargement de la vue tabulaire. Si la table contient des lignes de hauteur variable, il peut s'avérer coûteux de calculer toutes leurs hauteurs et donc de prolonger le temps de chargement. L'utilisation de l'estimation vous permet de reporter une partie du coût du calcul géométrique du temps de chargement au temps de défilement.

Donc, cette méthode est supposée être optionnelle. Vous penseriez que si vous sautiez cela, il retomberait sur le tableView:heightForRowAtIndexPath: exact, non? Mais si vous le ignorez sur iOS 8, vous obtiendrez ce comportement.

Qu'est-ce qui semble se passer? Je n'ai aucune connaissance interne, mais il semble que si vous n'implémentez pas cette méthode, UITableView traitera cela comme une hauteur de ligne estimée de 0. Cela compensera quelque peu cette situation (et, au moins dans certains cas, se plaindre dans le journal), mais vous verrez toujours une taille incorrecte. C'est évidemment un bogue dans UITableView. Vous voyez ce bogue dans certaines applications d’Apple, y compris quelque chose d'aussi basique que les paramètres.

Donc comment le répare-t-on? Fournir la méthode! Implémentez tableView: estimatedHeightForRowAtIndexPath:. Si vous ne disposez pas d'une estimation meilleure (et rapide), renvoyez simplement UITableViewAutomaticDimension. Cela corrigera complètement ce bogue.

Comme ça:

- (CGFloat)tableView:(UITableView *)tableView
estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
    return UITableViewAutomaticDimension;
}

Il y a des effets secondaires potentiels. Vous fournissez une très estimation approximative. Si vous constatez des conséquences (éventuellement, des cellules se déplaçant au fur et à mesure que vous faites défiler), vous pouvez essayer de renvoyer une estimation plus précise. (Rappelez-vous cependant: estimation.)

Cela dit, cette méthode n'est pas supposée pour retourner une taille parfaite, mais une taille suffisante. La vitesse est plus importante que la précision. Et pendant que j’avais repéré quelques problèmes de défilement dans le simulateur, il y avait aucun dans l’une de mes applications sur le périphérique réel, que ce soit sur iPhone ou sur iPad. (J’ai en fait essayé d’écrire une estimation plus précise. Mais il est difficile d’équilibrer la vitesse et la précision, et il n’y avait aucune différence observable dans aucune de mes applications. Elles fonctionnaient toutes aussi bien que de renvoyer UITableViewAutomaticDimension, ce qui était plus simple et suffisait à corrigez le bug.)

Je vous suggère donc de ne pas essayer de faire plus à moins que cela ne soit nécessaire. Faire plus si ce n'est pas nécessaire est plus susceptible de causer des bogues que de les résoudre. Vous pourriez finir par renvoyer 0 dans certains cas, et selon le moment où vous le renvoyez, cela peut entraîner la réapparition du problème initial.

La raison pour laquelle la réponse de Kai semble fonctionner est qu'elle implémente tableView:estimatedHeightForRowAtIndexPath: et évite ainsi l'hypothèse de 0. Et il ne renvoie pas 0 lorsque la vue est en train de disparaître. Cela dit, la réponse de Kai est trop compliquée, lente et sans plus de précision que de simplement renvoyer UITableViewAutomaticDimension. (Mais, encore une fois, merci Kai. Je n'aurais jamais compris cela si je n'avais pas vu votre réponse et si j'avais eu l'inspiration de la séparer et de comprendre pourquoi cela fonctionne.)]

Notez que vous devrez peut-être aussi forcer la présentation de la cellule. Vous penseriez que iOS le ferait automatiquement lorsque vous rendrez la cellule, mais ce n'est pas toujours le cas. (J'éditerai cela une fois que j'aurai un peu plus d'informations pour savoir quand vous devez le faire.)

Si vous avez besoin de faire cela, utilisez ce code avant return cell;:

[cell.contentView setNeedsLayout];
[cell.contentView layoutIfNeeded];
36
Steven Fisher

J'ai eu exactement le même problème. La vue tabulaire comportait plusieurs classes de cellules différentes, chacune d'une hauteur différente. De plus, l'une des classes de cellules devait afficher du texte supplémentaire, ce qui signifie une variation supplémentaire.

Le défilement était parfait dans la plupart des situations. Cependant, le même problème décrit dans la question s'est manifesté. En effet, après avoir sélectionné une cellule de tableau et présenté un autre contrôleur de vue, au retour à la vue de tableau d'origine, le défilement vers le haut était extrêmement saccadé.

La première ligne d’enquête consistait à examiner pourquoi les données étaient rechargées. Ayant expérimenté, je peux confirmer que lors du retour à la vue tableau, les données sont rechargées, bien que n'utilisant pas reloadData

Voir mon commentaire La vue de table ios 8 se recharge automatiquement lorsque celle-ci apparaît après un clic

En l'absence de mécanisme permettant de désactiver ce comportement, l'approche suivante consistait à étudier le défilement saccadé.

Je suis parvenu à la conclusion que les estimations retournées par estimatedHeightForRowAtIndexPath sont une estimation préalable. Connectez-vous à la console des estimations et vous verrez que la méthode déléguée est interrogée pour chaque ligne lorsque la vue Table apparaît pour la première fois. C'est avant tout défilement.

J'ai rapidement découvert qu'une partie de la logique d'estimation de hauteur de mon code était très erronée. La résolution de ce problème a résolu le pire des problèmes.

Pour obtenir un défilement parfait, j’ai adopté une approche légèrement différente des réponses ci-dessus. Les hauteurs ont été mises en cache, mais les valeurs utilisées proviennent des hauteurs réelles qui auraient été capturées lorsque l'utilisateur fait défiler l'écran vers le bas:

    var myRowHeightEstimateCache = [String:CGFloat]()

Ranger:

func tableView(tableView: UITableView, didEndDisplayingCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
    myRowHeightEstimateCache["\(indexPath.row)"] = CGRectGetHeight(cell.frame)
}

Utilisation de la cache:

func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat
{
    if let height = myRowHeightEstimateCache["\(indexPath.row)"]
    {
        return height
    } 
    else 
    {
    // Not in cache
    ... try to figure out estimate 
    }

Notez que dans la méthode ci-dessus, vous devrez renvoyer une estimation, cette méthode étant bien sûr appelée avant didEndDisplayingCell.

Mon hypothèse est qu'il y a une sorte de bogue Apple derrière tout cela. C'est pourquoi ce problème ne se manifeste que dans un scénario de sortie.

En bout de ligne, cette solution est très similaire à celles décrites ci-dessus. Cependant, j'évite les calculs compliqués et utilise le comportement UITableViewAutomaticDimension pour simplement mettre en cache les hauteurs de ligne réelles affichées à l'aide de didEndDisplayingCell.

TLDR: contournez le défaut le plus probable d'UIKit en mettant en cache la hauteur réelle des lignes. Interrogez ensuite votre cache en tant que première option de la méthode d'estimation.

8
Max MacLeod

En attendant que cela fonctionne, vous pouvez supprimer ces deux lignes:

self.tableView.estimatedRowHeight = 45
self.tableView.rowHeight = UITableViewAutomaticDimension

Et ajoutez cette méthode à votre viewController:

override func tableView(tableView: UITableView!, heightForRowAtIndexPath indexPath: NSIndexPath!) -> CGFloat {
    let cell = tableView.dequeueReusableCellWithIdentifier("cell") as TableViewCell
    cell.cellLabel.text = self.tableArray[indexPath.row]

    //Leading space to container margin constraint: 0, Trailling space to container margin constraint: 0
    let width = tableView.frame.size.width - 0
    let size = cell.cellLabel.sizeThatFits(CGSizeMake(width, CGFloat(FLT_MAX)))

    //Top space to container margin constraint: 0, Bottom space to container margin constraint: 0, cell line: 1
    let height = size.height + 1

    return (height <= 45) ? 45 : height
}

Cela a fonctionné sans autre changement dans votre projet de test.

7
Imanou Petit

Si vous avez défini la propriété estimatedRowHeight de tableView.

tableView.estimatedRowHeight = 100;

Alors commentez-le.

//  tableView.estimatedRowHeight = 100;

Il a résolu le bogue qui se produit dans iOS8.1 pour moi.

Si vous voulez vraiment le garder, vous pouvez forcer tableView à reloadData avant de pousser.

[self.tableView reloadData];
[self.navigationController pushViewController:vc animated:YES];

ou faites-le dans viewWillDisappear:.

- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    [self.tableView reloadData];
}

J'espère que ça aide.

3
tounaobun

Dans xcode 6 final pour moi la solution de contournement ne fonctionne pas. J'utilise des cellules personnalisées et j'exécute une cellule dans heightForCell, menant à une boucle infinie. Lorsque vous réduisez une cellule, vous appelez heightForCell. 

Et toujours le bogue semble être présent.

2
Tuan

Si rien de ce qui précède n'a fonctionné pour vous (comme cela m'est arrivé), vérifiez simplement que la propriété estimatedRowHeight à partir de la vue Table est assez précise. J'ai vérifié que j'utilisais 50 pixels alors qu'il était plus proche de 150 pixels. La mise à jour de cette valeur a résolu le problème!

tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = tableViewEstimatedRowHeight // This should be accurate.
0
jomafer