UITableView desplazamiento entrecortado SIN imágenes o búsqueda de contenido

Así que tengo este UITableView que obtiene sus datos de la memory (ya está precargado, no hay requestes activas mientras se desplaza, todo está cargado antes de que se distribuya la vista). Cada celda tiene su altura calculada dinámicamente en function de la cantidad de text en UITextView y Autolayout. Las celdas se cargan desde una punta y las celdas de reutilización funcionan correctamente (al less así lo espero). Utilizo UITableViewAutomaticDimension al calcular la altura de las filas, por lo que no forzo células al layout dos veces como antes con iOS 8.

Aquí están los methods relevantes donde relleno las celdas y calculo las alturas:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { NSString *cellType = [self reuseIdentifierForIndexPath:indexPath]; if ([cellType isEqualToString:kLoadingCell]) return kLoadingCellHeight; else if ([cellType isEqualToString:kOfflineCell]) return kOfflineCellHeight; else if ([cellType isEqualToString:kFootprintListHeaderCell]) return kHeaderCellHeight; else if ([cellType isEqualToString:kFootprintCellUnsynced]) return kUnsyncedCellHeight; else if ([cellType isEqualToString:kShowFullTripCell]) return kShowFullTripCellHeight; else if ([cellType isEqualToString:kFootprintOnMapCell]) return kFootprintOnMapCellHeight; else { return UITableViewAutomaticDimension; } } - (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath { NSString *cellType = [self reuseIdentifierForIndexPath:indexPath]; if ([cellType isEqualToString:kLoadingCell]) return kLoadingCellHeight; else if ([cellType isEqualToString:kOfflineCell]) return kOfflineCellHeight; else if ([cellType isEqualToString:kFootprintListHeaderCell]) return kHeaderCellHeight; else if ([cellType isEqualToString:kFootprintCellUnsynced]) return kUnsyncedCellHeight; else if ([cellType isEqualToString:kShowFullTripCell]) return kShowFullTripCellHeight; else if ([cellType isEqualToString:kFootprintOnMapCell]) return kFootprintOnMapCellHeight; else { return UITableViewAutomaticDimension; } } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { NSString *cellType = [self reuseIdentifierForIndexPath:indexPath]; if ([cellType isEqualToString:kLoadingCell]) { UITableViewCell *loadingCell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil]; loadingCell.tag = kLoadingCellTag; loadingCell.selectionStyle = UITableViewCellSelectionStyleNone; loadingCell.backgroundColor = loadingCell.contentView.backgroundColor = [UIColor clearColor]; UIActivityIndicatorView *activityIndicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite]; activityIndicatorView.center = CGPointMake(tableView.frame.size.width / 2, 20); [loadingCell.contentView addSubview:activityIndicatorView]; [activityIndicatorView startAnimating]; return loadingCell; } else if ([cellType isEqualToString:kOfflineCell]) { FPOfflineCell *offlineCell = [tableView dequeueReusableCellWithIdentifier:kOfflineCell]; return offlineCell; } else if ([cellType isEqualToString:kFootprintListHeaderCell]) { FPFootprintListHeaderCell *headerCell = [tableView dequeueReusableCellWithIdentifier:kFootprintListHeaderCell]; [headerCell.syncButton addTarget:self action:@selector(syncButtonPressed:) forControlEvents:UIControlEventTouchUpInside]; return headerCell; } else if ([cellType isEqualToString:kFootprintCellUnsynced]) { FPFootprintCellUnsynced *unsyncedCell = [tableView dequeueReusableCellWithIdentifier:kFootprintCellUnsynced]; unsyncedCell.footprint = self.map.footprintsNonSynced[[self unsyncedFootprintIndexForIndexPath:indexPath]]; return unsyncedCell; } else if ([cellType isEqualToString:kShowFullTripCell]) { FPShowFullTripCell *showFullTripCell = [tableView dequeueReusableCellWithIdentifier:kShowFullTripCell]; return showFullTripCell; } else if ([cellType isEqualToString:kFootprintOnMapCell]) { FPFootprintOnMapCell *footprintOnMapCell = [tableView dequeueReusableCellWithIdentifier:kFootprintOnMapCell]; footprintOnMapCell.footprint = self.map.footprints[0]; return footprintOnMapCell; } else { FPFootprint *footprint = self.map.footprints[[self footprintIndexForIndexPath:indexPath]]; FootprintCell *cell = [tableView dequeueReusableCellWithIdentifier:kFootprintCell]; cell.titleLabel.text = footprint.name; cell.dateLabel.text = footprint.displayDate; cell.textView.text = nil; if (footprint.text && footprint.text.length > 0) { if ([self.readmoreCache[@(footprint.hash)] boolValue]) { cell.textView.text = footprint.text; } else { cell.textView.text = [footprint.text stringByAppendingReadMoreAndLimitingToCharacterCount:300 screenWidth:tableView.frame.size.width]; } } else { cell.hasText = NO; } cell.textView.markdownLinkTextViewDelegate = self; [cell.textView setNeedsDisplay]; cell.isPrivate = footprint.isPrivate; [cell.likesAndCommentsView setLikesCount:footprint.likes andCommentsCount:footprint.comments]; [cell.likesAndCommentsView setLiked:footprint.liked]; [cell.likesAndCommentsView.likeButton addTarget:self action:@selector(likeButtonPressed:) forControlEvents:UIControlEventTouchUpInside]; [cell.likesAndCommentsView.likesTextButton addTarget:self action:@selector(likesTextButtonPressed:) forControlEvents:UIControlEventTouchUpInside]; [cell.likesAndCommentsView.commentButton addTarget:self action:@selector(commentButtonPressed:) forControlEvents:UIControlEventTouchUpInside]; [cell.likesAndCommentsView.commentsTextButton addTarget:self action:@selector(commentsTextButtonPressed:) forControlEvents:UIControlEventTouchUpInside]; [cell.detailButton addTarget:self action:@selector(detailButtonPressed:) forControlEvents:UIControlEventTouchUpInside]; [cell.translateButton addTarget:self action:@selector(translateButtonPressed:) forControlEvents:UIControlEventTouchUpInside]; if (footprint.canBeTranslated) { cell.translationStatus = footprint.translationState; if (footprint.translationState == FPFootprintTranslationStateTranslated) { cell.translatedTextView.text = footprint.translatedText; } } else { cell.translationStatus = FPFootprintTranslationStateNotAvailible; } cell.numberOfImages = 2; return cell; } } 

Y esta es mi celda:

 import UIKit @objc class FootprintCell: UITableViewCell { var translationStatus: FPFootprintTranslationState = .NotTranslated { didSet { translateButton.hidden = true translateLoader.stopAnimating() translatedTextView.hidden = true translatedTextView.text = nil translatedTextView.addConstraint(translatedTextViewHeightConstraint) translationButtonHeightConstraint.constant = 0 loaderHeightConstraint.constant = 0 switch translationStatus { case .NotAvailible: break case .NotTranslated: translateButton.hidden = false translationButtonHeightConstraint.constant = translationButtonHeightConstraintConstant case .Translating: translateLoader.startAnimating() loaderHeightConstraint.constant = loaderHeightConstraintConstant translatedTextView.text = nil case .Translated: translatedTextView.hidden = false translatedTextView.removeConstraint(translatedTextViewHeightConstraint) } } } var isPrivate: Bool = false { didSet { privacyBar.hidden = !isPrivate privacyIcon.image = UIImage(named: isPrivate ? "ic_lock" : "ic_globe") } } var hasText: Bool = true { didSet { if hasText { textView.removeConstraint(textViewHeightConstraint) } else { textView.addConstraint(textViewHeightConstraint) } } } var numberOfImages: Int = 0 { didSet { if numberOfImages == 0 { imagesContainer.subviews.map { $0.removeFromSuperview() } } else if numberOfImages == 2 { twoImagesContainer = NSBundle.mainBundle().loadNibNamed("FootprintCellTwoImagesContainer", owner: nil, options: nil)[0] as? FootprintCellTwoImagesContainer twoImagesContainer?.setTranslatesAutoresizingMaskIntoConstraints(false) imagesContainer.addSubview(twoImagesContainer!) let views = ["foo" : twoImagesContainer!] as [NSString : AnyObject] imagesContainer.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[foo]|", options: .allZeros, metrics: nil, views: views)) imagesContainer.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[foo]|", options: .allZeros, metrics: nil, views: views)) } } } @IBOutlet private(set) weak var titleLabel: UILabel! @IBOutlet private(set) weak var dateLabel: UILabel! @IBOutlet private(set) weak var textView: FPForwardingTextView! @IBOutlet private(set) weak var likesAndCommentsView: FPLikesAndCommentsView! @IBOutlet private weak var privacyBar: UIView! @IBOutlet private weak var privacyIcon: UIImageView! @IBOutlet private(set) weak var detailButton: UIButton! @IBOutlet private(set) weak var translateButton: UIButton! @IBOutlet private weak var translateLoader: UIActivityIndicatorView! @IBOutlet private(set) weak var translatedTextView: FPForwardingTextView! @IBOutlet private(set) weak var imagesContainer: UIView! private(set) var twoImagesContainer: FootprintCellTwoImagesContainer? @IBOutlet private weak var translationButtonHeightConstraint: NSLayoutConstraint! @IBOutlet private weak var loaderHeightConstraint: NSLayoutConstraint! @IBOutlet private var translatedTextViewHeightConstraint: NSLayoutConstraint! @IBOutlet private var textViewHeightConstraint: NSLayoutConstraint! private var translationButtonHeightConstraintConstant: CGFloat! private var loaderHeightConstraintConstant: CGFloat! override func awakeFromNib() { super.awakeFromNib() textView.contentInset = UIEdgeInsets(top: -10, left: -5, bottom: 0, right: 0) textView.linkColor = UIColor(fromHexString: "0088CC") translatedTextView.contentInset = UIEdgeInsets(top: -10, left: -5, bottom: 0, right: 0) translatedTextView.linkColor = UIColor(fromHexString: "0088CC") privacyBar.backgroundColor = UIColor(patternImage: UIImage(named: "ic_privacy_bar")) translatedTextView.text = nil translatedTextView.hidden = true translateButton.hidden = true translationButtonHeightConstraintConstant = translationButtonHeightConstraint.constant loaderHeightConstraintConstant = loaderHeightConstraint.constant hasText = true } func layoutMargins() -> UIEdgeInsets { return UIEdgeInsetsZero } override func prepareForReuse() { super.prepareForReuse() numberOfImages = 0 translationStatus = .NotAvailible hasText = true } } 

FootprintCellTwoImagesContainer y FPLikesAndCommentsView se cargan desde Nibs y actualmente no contienen imágenes ni cargan nada, solo algunos Autolayout.

El problema principal es incluso cuando se carga toda la tablaView y cada celda se muestra al less una vez (por lo que debería haber suficientes celdas para reutilizarlo), después de desplazarse LENTAMENTE por un borde de celda hacia arriba o hacia abajo, obtengo un pequeño salto (como 5 píxeles arriba y abajo). Esto sucede en todos los dispositivos, incluso en un 6 Plus.

¿Alguna idea de dónde podría ser el problema? Espero que no sea algo con mis limitaciones en las xibs, al less Interface Builder no lanza advertencias allí …

No estoy tan seguro de que UITableViewAutomaticDimension sea para celdas de tabla. De la documentation …

Devuelve este valor de los methods UITableViewDelegate que solicitan métricas de dimensión cuando desea que UITableView elija un valor pnetworkingeterminado. Por ejemplo, si devuelve esta constante en tableView: heightForHeaderInSection: o tableView: heightForFooterInSection :, UITableView usa una altura que se ajusta al valor devuelto de tableView: titleForHeaderInSection: o tableView: titleForFooterInSection: (si el título no es nulo).

Sin mencionar las celdas de table.

Entonces hice una búsqueda y encontré esta … más discusión sobre UITableViewAutomaticDimension …

En donde dice..

no funcionará. UITableViewAutomaticDimension no está destinado a ser utilizado para establecer la altura de la fila. Use rowHeight y especifique su valor o implementar:

Entonces, creo que tal vez te equivoques.

Bien antes del código, el principio. Tengo celdas personalizadas con 4 tags en una columna. La label superior (label1) siempre tiene text y la label inferior (label4) también siempre tiene text. Las tags 2 y 3 pueden contener text, eso es uno, o ambos pueden. Para lograr el cambio de tamaño, utilizamos methods de layout parcial de parte y de delegación de parte (no muy lejos de lo que tenemos)

En el generador de interfaces, establecemos las restricciones para la celda prototipo

Label1: Leading, Trailing, top, height, width

Label2: Leading, Trailing, top, bottom, height, width

Label3: Leading, Trailing, top, bottom, height, width

Label4: Leading, Trailing, top, bottom, height, width

Para las tags 1 y 4 (arriba y abajo) establecemos la prioridad de resistencia de compression de contenido vertical en 'requerido' (1000). También para las tags 2 y 3 establecemos la prioridad de resistencia de compression de contenido vertical a 'bajo' (250)

Esto básicamente significa que si la altura debe disminuir, queuepsar las tags 2 y 3 primero y más arriba que queuepsan las tags 1 y 4. (Puede que ya sepas todo esto) No deberías tener advertencias y todas las restricciones se agregaron correctamente. (no use restricciones a los márgenes a less que sepa lo que hace)

Ahora el codigo

 - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { // Calls the sizing method to return a calculated height. return [self heightForBasicCellAtIndexPath:indexPath]; 

}

 - (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath { //taken from interface builder. If all 4 labels have strings and not collapsed, this is the height the cell will be. return 123.0f; 

}

 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { // Yours has lots of case logic - // Mine is similar but I configure the properties of the custom cell elsewhere mainly so it can be used for sizing. [self configureCell:cell atIndexPath:indexPath]; return cell; 

}

 - (void)configureCell:(MyJobCell *)cell atIndexPath:(NSIndexPath *)indexPath { // my data source MyCase *aCase = [_fetchedResultsController objectAtIndexPath:indexPath]; // setting the labels to match the case from data cell.label1.text = aCase.name; cell.label2.text = aCase.address; cell.label3.text = aCase.postcode; cell.label4.text = aCase.caseDescription; 

}

 - (CGFloat)heightForBasicCellAtIndexPath:(NSIndexPath *)indexPath { // In here I create a cell and configure it with a cell identifier static MyJobCell *sizingCell = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sizingCell = [self.tableView dequeueReusableCellWithIdentifier:MyJobCellIdentifier]; }); // This configures the sizing cell labels with text values [self configureCell:sizingCell atIndexPath:indexPath]; // This line calls the calculation. It fires the Auto Layout constraints on the cell, // If label 2 and / or label 3 are empty, they will be collapsed to 0 height. return [self calculateHeightForConfigunetworkingSizingCell:sizingCell]; 

}

 - (CGFloat)calculateHeightForConfigunetworkingSizingCell:(MYJobCell *)sizingCell { //Force the cell to update its constraints [sizingCell setNeedsLayout]; [sizingCell layoutIfNeeded]; // Get the size of the 'squashed' cell and return it to caller CGSize size = [sizingCell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize]; return size.height; 

}

Es lo mejor que puedo hacer para mostrarle un método de trabajo. Tendrá que hacer ajustes lógicamente para atender a todos los diferentes types de celdas personalizadas que tiene. Pero aparte de eso, creo que esto debería ayudar. Déjame saber cómo te va.