【问题标题】:UITableView choppy scrolling WITHOUT images or content fetching没有图像或内容获取的 UITableView 断断续续的滚动
【发布时间】:2014-11-27 04:11:52
【问题描述】:

所以我得到了这个 UITableView,它从内存中获取数据(已经预加载,滚动时没有请求,所有内容都在视图布局之前加载)。每个单元格的高度是根据 UITextView 和 Autolayout 中的文本量动态计算的。单元格是从笔尖加载的,并且重复使用单元格工作正常(至少我希望如此)。我在计算行高时使用 UITableViewAutomaticDimension,因此我不会像在 iOS 8 之前那样强制单元格布局两次。

这是我填充单元格并计算高度的相关方法:

- (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;
    }
}

这是我的牢房:

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
    }

}

FootprintCellTwoImagesContainerFPLikesAndCommentsView 是从 Nibs 加载的,目前不包含任何图像或加载任何内容,只是一些自动布局。

所以主要问题是即使加载了整个 tableView 并且每个单元格至少显示一次(因此应该有足够的单元格可以重复使用),在向上或向下缓慢滚动单元格边框后,我得到了一个小跳跃(比如上下 5 个像素)。这发生在每台设备上,甚至在 6 Plus 上。

任何想法可能是问题所在?我希望这不是我在 xibs 中的限制,至少 Interface Builder 不会在那里抛出警告......

【问题讨论】:

  • 在 Instruments 中运行 Time Profiler。这里的代码太多,看不到任何具体的东西。 Time Profiler 会告诉你代码瓶颈在哪里。
  • 您的 heightForRowAtIndexPath 和estimatedHeightForRowAtIndexPath 几乎都在做同样的事情,因此实现这一点没有任何好处。您是否正在尝试调整高度可变的单元格的大小?如果是这样,我可以向您展示一个可以解决问题的快速调整。
  • @latenitecoder 好的,进展如何?是的,我正在尝试这种布局,这就是我使用 UITableViewAutomaticDimension 的原因。

标签: ios objective-c uitableview swift autolayout


【解决方案1】:

我不太确定 UITableViewAutomaticDimension 是否适用于表格单元格。从文档中...

当您希望 UITableView 选择默认值时,您从请求维度指标的 UITableViewDelegate 方法返回此值。例如,如果您在 tableView:heightForHeaderInSection: 或 tableView:heightForFooterInSection: 中返回此常量,则 UITableView 使用适合从 tableView:titleForHeaderInSection: 或 tableView:titleForFooterInSection: 返回的值的高度(如果标题不为零)。

没有提到表格视图单元格。

于是我搜索了一下,发现了这个... more discussion on UITableViewAutomaticDimension...

上面写着..

它不会起作用。 UITableViewAutomaticDimension 不打算用于设置行高。使用 rowHeight 并指定您的值或实施:

所以我认为你可能错了。

【讨论】:

    【解决方案2】:

    OK 之前的代码,原理。我有一列中有 4 个标签的自定义单元格。 顶部标签 (label1) 始终包含文本,底部标签 (label4) 也始终包含文本。标签 2 和 3 可能包含文本,即一个可能,或两者都可能。 为了实现大小调整,我们使用部分自动布局和部分委托方法(与您所拥有的不远)

    在Interface builder中,我们为原型单元设置了约束

    Label1:前导、尾随、顶部、高度、宽度

    Label2:前导、尾随、顶部、底部、高度、宽度

    Label3:前导、尾随、顶部、底部、高度、宽度

    Label4:前导、尾随、顶部、底部、高度、宽度

    对于标签 1 和 4(顶部和底部),我们将内容压缩阻力优先级垂直设置为“必需”(1000) 同样对于标签 2 和 3,我们将内容压缩阻力优先级垂直设置为“低”(250)

    这基本上意味着如果高度应该减小,首先折叠标签 2 和 3,然后折叠标签 1 和 4。(您可能已经知道这一切) 您应该没有警告,并且您的约束都正确添加。 (除非你知道它的作用,否则不要使用对边距的约束)

    现在是代码。

    - (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 calculateHeightForConfiguredSizingCell:sizingCell];
    

    }

    - (CGFloat)calculateHeightForConfiguredSizingCell:(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;
    

    }

    这是我能做的最好的向您展示一种工作方法的方法。您将不得不进行调整以从逻辑上满足您拥有的所有不同类型的自定义单元格。 但除此之外,我认为这应该有所帮助。让我知道你的进展情况。

    【讨论】:

    • 是的,这就是我在 iOS8 之前的样子。我在某个地方有一个原型单元,并在应该调用高度时用它来布局所有内容。但是现在 UIKit 使用 UITableViewAutomaticDimension 为您完成工作。当我查看 Apple 使用内容繁重的单元格的示例时,我发现他们使用绘图方法,这确实更快,但我的单元格中也有一些按钮和可点击的图像,并且在文本视图中也有可能的链接,所以绘图不是选项。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-01-03
    • 1970-01-01
    • 2021-06-12
    • 1970-01-01
    相关资源
    最近更新 更多