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