Ios UITableview自定义单元格在末尾重新加载、结巴、被切成两半,并且在滚动期间不重新加载

Ios UITableview自定义单元格在末尾重新加载、结巴、被切成两半,并且在滚动期间不重新加载,ios,objective-c,uitableview,custom-cell,Ios,Objective C,Uitableview,Custom Cell,我已经创建了一个带有自定义单元格的UItableview。这些单元格有两个UILabel和一个imageview,用于调用背景线程上的图像。单元格首先加载一些占位符数据,以防API调用花费很长时间。然后,使用通知,使用新数据重新加载tableview。当应用程序在iPhone上启动时,它会上下滚动。然而,在上下快速滚动之后,单元格开始崩溃。有时,在当前列表下方会生成一个全新的列表。其他时候,最后一个单元格可能会被切成两半。这是什么原因?你能帮忙吗?多谢各位 TableViewVC.m: - (

我已经创建了一个带有自定义单元格的UItableview。这些单元格有两个UILabel和一个imageview,用于调用背景线程上的图像。单元格首先加载一些占位符数据,以防API调用花费很长时间。然后,使用通知,使用新数据重新加载tableview。当应用程序在iPhone上启动时,它会上下滚动。然而,在上下快速滚动之后,单元格开始崩溃。有时,在当前列表下方会生成一个全新的列表。其他时候,最后一个单元格可能会被切成两半。这是什么原因?你能帮忙吗?多谢各位

TableViewVC.m:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return [self.appEntries count];
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {

    return CELL_HEIGHT;
}


- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    ELAppCell *elAppCell = (ELAppCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    AppEntry *appEntry = [self.appEntries objectAtIndex:indexPath.row];

    if (!elAppCell) {
        elAppCell = [[ELAppCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier appEntry:appEntry];
    }
    else {
        [elAppCell configureCellWithAppEntry:appEntry];
    }
    return elAppCell;
}
和自定义单元格类:

@implementation ELAppCell

- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier appEntry:(AppEntry *)appEntry
{
    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
    if (self) {

        self.appNameLabel = [self buildAppNameLabel:appEntry.name];
        self.thumbnailImageView = [self buildThumbnailImageView:appEntry.smallPictureURl];
        self.appArtistLabel = [self buildAppArtistLabel:appEntry.artist];

        [self.contentView addSubview:self.appNameLabel];
        [self.contentView addSubview:self.thumbnailImageView];
        [self.contentView addSubview:self.appArtistLabel];
    }
    return self;
}

- (void)configureCellWithAppEntry:(AppEntry *)appEntry{

    dispatch_queue_t fetchQ = dispatch_queue_create("ConfigureCell", NULL);
    dispatch_async(fetchQ, ^{

        NSURL *address = [NSURL URLWithString:appEntry.smallPictureURl];
        UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:address]];
        dispatch_async(dispatch_get_main_queue(), ^{

            self.appNameLabel.text = appEntry.name;
            self.thumbnailImageView.image = image;
            self.appArtistLabel.text = [NSString stringWithFormat:@"By %@", appEntry.artist];

        });
    });
}

您的代码有一个基本问题。考虑在视图控制器中获取图像。当前代码可能会遇到的问题是,如果加载图像需要很长时间,则有可能(在用户快速滚动的情况下,这种可能性非常高)单元格会被排在另一行的渲染队列中,但前一行的图像只会被下载并显示,这将导致UI不一致

ELAppCell
中有两个地方会导致所述错误:

  • -(void)配置CellWithAppentry:(AppEntry*)AppEntry{
    ,我们已经修复了它
  • >(uIIVIEVIEW*)BuffdTynNaveIVIEVIEW:(NSCORL**)IVIULIL/<代码>您应该删除(或注释)所有的调度调用。对<代码>调度……/代码>的调用是多余的,并导致bug;TabLVIEW控制器负责加载图像。如果需要加载小图像,那么也考虑将它们加载到视图控制器中。
ELAppListTableVC
中还有一个严重的错误,您应该删除
appListTableView
属性以及代码中使用它的所有位置。在
receiveEvent:notification
方法中,您应该调用
[self.tableView reloadData];
而不是重新加载
appListTableView
。基本上,您有两个表视图彼此重叠,其中
appListTableView
是手动添加的。注意:您的
ELAppListTableVC
已经从
UITableViewController
继承,因此它已经有了由视图自动实例化的表视图控制器

加载图像的代码可以移动到view controller或作为模型类的一部分。详细信息如下:

在视图控制器中加载图像 加载映像后,您需要使用特定索引路径更新单元格,因为索引路径表示下载映像的AppEntry实例。这意味着您应该要求table view返回表示该索引路径的单元格。它可能会使该单元格当前不可见,在这种情况下,您将获得
nil
。因此在cal中下载完成后在主队列上调用的lback块,您需要有对该索引路径的引用。因为代码比上百个世界都好,下面是它的外观:

- (UITableViewCell *)tableView:(UITableView *)tableView 
         cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    ELAppCell *elAppCell = (ELAppCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    AppEntry *appEntry = [self.appEntries objectAtIndex:indexPath.row];

    if (!elAppCell) {
        elAppCell = [[ELAppCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier appEntry:appEntry];
    } 

    [elAppCell configureCellWithAppEntry:appEnty];

    self.thumbnailImageView.image = nil;
    dispatch_queue_t fetchQ = dispatch_queue_create("ConfigureCell", NULL);
    dispatch_async(fetchQ, ^{
        NSURL *address = [NSURL URLWithString:appEntry.smallPictureURl];
        UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:address]];
        dispatch_async(dispatch_get_main_queue(), ^{
            ELAppCell *updateCell = (ELAppCell *)[tableView cellForRowAtIndexPath:indexPath];
            if (updateCell) { // if nil then cell is not visible hence no need to update
                updateCell.thumbnailImageView.image = image;
            }
        });
    });

    return elAppCell;
}
重构此
后,configureCellWithAppEntry:
代码应如下所示:

- (void)configureCellWithAppEntry:(AppEntry *)appEntry {
    self.appNameLabel.text = appEntry.name;
    self.appArtistLabel.text = [NSString stringWithFormat:@"By %@", appEntry.artist];
}
代码要好得多,但仍然需要重新下载用户回滚时已下载的图像

将图像加载代码移动到模型类(对上述代码的进一步改进) 每次显示单元格时,您都会执行下载,即已下载的图像将再次下载。要缓存图像,您可以向AppEntry类添加
thumbnailImage
属性,并仅在用户需要时延迟加载此图像。为此,您可以使用下面的代码更新AppEntry类:

AppEntry.h

@interface AppEntry // if it is CoreData object here should be AppEntry: NSManagedObject

@property (nonatomic, readonly) UIImage *thumbnailImage;

- (void)loadThumbnailImage:(void (^)())completionBlock;

@end
AppEntry.m

@interface AppEntry ()

@property (nonatomic, readwrite) UIImage *thumbnailImage;

@end

@implementation AppEntry 

- (void)loadThumbnailImage:(void (^)())completionBlock {
    dispatch_queue_t fetchQ = dispatch_queue_create("ConfigureCell", NULL);
    dispatch_async(fetchQ, ^{
        NSURL *address = [NSURL URLWithString:self.smallPictureURl];
        UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:address]];
        dispatch_async(dispatch_get_main_queue(), ^{
            self.thumbnailImage = image;
            completionBlock();
        });
    });
}

@end
当然,还应更新视图控制器:

- (UITableViewCell *)tableView:(UITableView *)tableView 
         cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    ELAppCell *elAppCell = (ELAppCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    AppEntry *appEntry = [self.appEntries objectAtIndex:indexPath.row];

    if (!elAppCell) {
        elAppCell = [[ELAppCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier appEntry:appEntry];
    } 

    [elAppCell configureCellWithAppEntry:appEnty];

    if (appEntry.thumbnailImage) {
        elAppCell.thumbnailImageView.image = appEntry.thumbnailImage;
    } else {
        [appEntry loadThumbnailImage: ^{            
            elAppCell.thumbnailImageView.image = appEntry.thumbnailImage;
        }];
    }

    return elAppCell;
}
一旦我们在
AppEntry
模型中有了缩略图,我们就可以更新单元格代码,以便在
configureCellWithAppEntry:
方法中使用它:

- (void)configureCellWithAppEntry:(AppEntry *)appEntry{
    self.appNameLabel.text = appEntry.name;
    self.appArtistLabel.text = [NSString stringWithFormat:@"By %@", appEntry.artist];
    self.thumbnailImageView.image = appEntry.thumbnailImage;
}
即使使用图像缓存,当用户可以快速上下滚动
thumbnailImage
时,我们也可以重新下载
AppEntry
实例的
nil
,因为图像尚未下载。因此,我们需要如何处理图像下载状态,详情如下

图像下载和缓存的优化 要解决连续调用
loadThumbnailImage:
的问题,当我们执行新下载时,我们需要处理
downloadInProgress
状态。
AppEntry
需要调整为:

@implementation AppEntry {
    dispatch_group_t _thumbnailDownloadGroup;
    dispatch_queue_t _thumbnailFetchQueue;
    dispatch_once_t _thumbnailQOnceToken;
    BOOL _loadingThumbnail;
}

- (void)loadThumbnailImage:(void (^)())completionBlock {
    if (self.thumbnailImage) { // return immediately since we have image
        completionBlock();
        
    } else if (_loadingThumbnail) { // return when previously requested download complete
        dispatch_group_notify(_thumbnailDownloadGroup, _thumbnailFetchQueue, ^{
            completionBlock();
        });
        
    } else { // download image
        dispatch_once(&_thumbnailQOnceToken, ^{
            _thumbnailDownloadGroup = dispatch_group_create();
            _thumbnailFetchQueue = dispatch_queue_create("ThumbnailDownload", NULL);
        });
        
        __weak typeof(self) weakSelf = self;
        dispatch_group_async(_thumbnailDownloadGroup, _thumbnailFetchQueue, ^{
            NSURL *address = [NSURL URLWithString:weakSelf.smallPictureURl];
            UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:address]];
            dispatch_async(dispatch_get_main_queue(), ^{
                self.thumbnailImage = image;
                completionBlock();
            });
        });
    }
}

@end

我还没有运行要测试的示例,但我希望您能明白这一点。

您没有必要获得1分,事实上可能是错误的,这取决于OP是如何生成单元格的。较新的方法(使用forIndexPath)必须返回单元格,如果不返回,则会崩溃。因为看起来OP将进入他的“if(!elAppCell)”中子句,那么他必须在出列时返回nil。因此,他应该在他的特定情况下使用旧方法。是的,你是对的。从答案中删除该部分。感谢@Keenle,澄清一下,你建议将图像抓取移动到视图控制器还是移动到模型?第一段说的是前者,第二段说的是后者。我很抱歉我已经扩展了答案。现在它看起来像是一个为表视图单元格异步加载图像的教程。希望它能帮助您改进代码。@Keenle我已经按照您在第一个示例中的建议将图像加载移到了VC中。不幸的是,这并不能解决问题。现在,当我向下滚动并尝试再滚动一些时,一个new单元格集生成在最后一个单元格的下方。当我滚动到顶部时,我得到半个单元格(不是第一个输入的单元格)。您是否打算将MYTableViewCell设置为ELAppCell,将cell设置为ELAppCell?当我将其更改为我提供的名称时,我将遇到一个崩溃-***由于未捕获的异常“NSInternalInconsistencException”而终止应用程序,原因是:“无法加载NIB”
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    NSString *CellIdentifier = [NSString stringWithFormat:@"cell %ld %ld",(long)indexPath.row,(long)indexPath.section];

    ELAppCell *elAppCell = (ELAppCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    elAppCell=nil;

    AppEntry *appEntry = [self.appEntries objectAtIndex:indexPath.row];


    if (elAppCell == nil)
    {

        NSArray *topLevelObjects = [[NSBundle mainBundle] loadNibNamed:@"ELAppCell" owner:nil options:nil];

        for(id currentObject in topLevelObjects)
        {
            if([currentObject isKindOfClass:[MYTableViewCell class]])
            {
                cell = (MYTableViewCell *)currentObject;
                break;
            }
        }


        elAppCell = [[ELAppCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier appEntry:appEntry];

    }
    return elAppCell;

}