LazyTable是apple一个演示tableview懒加载图片的demo。它通过下载apple 应用商店付费榜单的信息数据,解析并且显示在cell中,其中应用的icon采用懒加载方式进行。整个效果非常流畅。
在appdelegate中:
首先在应用启动时建立下载请求并指示网络活动中:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
NSURLRequest *urlRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:TopPaidAppsFeed]];
self.appListFeedConnection = [[NSURLConnection alloc] initWithRequest:urlRequest delegate:self];
NSAssert(self.appListFeedConnection != nil, @"Failure to create URL connection.");
// show in the status bar that network activity is starting
[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
return YES;
}
接着是四个NSURLConnectionDelegate 方法,其中提供了错误处理,错误又区分了网络无连接和其他情况,统一使用错误处理方法来显示错误信息。在下载完成的代理方法中新建了一个operate queue,一个parse operation。并且设定parse operation要解析的数据,错误、成功解析后的处理block,并将parse operation添加到operate queue。
// -------------------------------------------------------------------------------
// connectionDidFinishLoading:connection
// -------------------------------------------------------------------------------
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
self.appListFeedConnection = nil; // release our connection
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
// create the queue to run our ParseOperation
self.queue = [[NSOperationQueue alloc] init];
// create an ParseOperation (NSOperation subclass) to parse the RSS feed data
// so that the UI is not blocked
ParseOperation *parser = [[ParseOperation alloc] initWithData:self.appListData];
parser.errorHandler = ^(NSError *parseError) {
dispatch_async(dispatch_get_main_queue(), ^{
[self handleError:parseError];
});
};
// Referencing parser from within its completionBlock would create a retain
// cycle.
__weak ParseOperation *weakParser = parser;
parser.completionBlock = ^(void) {
if (weakParser.appRecordList) {
// The completion block may execute on any thread. Because operations
// involving the UI are about to be performed, make sure they execute
// on the main thread.
dispatch_async(dispatch_get_main_queue(), ^{
// The root rootViewController is the only child of the navigation
// controller, which is the window's rootViewController.
RootViewController *rootViewController = (RootViewController*)[(UINavigationController*)self.window.rootViewController topViewController];
rootViewController.entries = weakParser.appRecordList;
// tell our table view to reload its data, now that parsing has completed
[rootViewController.tableView reloadData];
});
}
// we are finished with the queue and our ParseOperation
self.queue = nil;
};
[self.queue addOperation:parser]; // this will start the "ParseOperation"
// ownership of appListData has been transferred to the parse operation
// and should no longer be referenced in this thread
self.appListData = nil;
}
对于retain cycle问题,可以参考http://www.cocoachina.com/macdev/cocoa/2013/0527/6285.html
在RootViewController中
这里主要是作为tableview的delegate和datasource,实现这些方法。特别关注的是方法- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath。
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
// customize the appearance of table view cells
//
static NSString *CellIdentifier = @"LazyTableCell";
static NSString *PlaceholderCellIdentifier = @"PlaceholderCell";
// add a placeholder cell while waiting on table data
NSUInteger nodeCount = [self.entries count];
if (nodeCount == 0 && indexPath.row == 0)
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:PlaceholderCellIdentifier];
cell.detailTextLabel.text = @"Loading…";
return cell;
}
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
// Leave cells empty if there's no data yet
if (nodeCount > 0)
{
// Set up the cell...
AppRecord *appRecord = [self.entries objectAtIndex:indexPath.row];
cell.textLabel.text = appRecord.appName;
cell.detailTextLabel.text = appRecord.artist;
// Only load cached images; defer new downloads until scrolling ends
if (!appRecord.appIcon)
{
if (self.tableView.dragging == NO && self.tableView.decelerating == NO)
{
[self startIconDownload:appRecord forIndexPath:indexPath];
}
// if a download is deferred or in progress, return a placeholder image
cell.imageView.image = [UIImage imageNamed:@"Placeholder.png"];
}
else
{
cell.imageView.image = appRecord.appIcon;
}
}
return cell;
}
这里声明了两种cell标识,一种是下载数据时的占位cell,一种是懒加载cell。textLabel和detailTextLabel的text都可以从解析结果里找出来,但是对于appIcon,我们现在还只有它的地址。通过判断appRecord.appIcon是否为空来决定是否下载,当然还要过滤几种情况:tableview正在拖曳和减速,对于这两种情况就直接传一个占位图。因为我们这里拦截了正在拖拽和正在减速两种情况,我们还得处理拖拽完成并且没有减速动作和减速完成时的处理,都调用loadImagesForOnscreenRows方法
// -------------------------------------------------------------------------------
// loadImagesForOnscreenRows
// This method is used in case the user scrolled into a set of cells that don't
// have their app icons yet.
// -------------------------------------------------------------------------------
- (void)loadImagesForOnscreenRows
{
if ([self.entries count] > 0)
{
NSArray *visiblePaths = [self.tableView indexPathsForVisibleRows];
for (NSIndexPath *indexPath in visiblePaths)
{
AppRecord *appRecord = [self.entries objectAtIndex:indexPath.row];
if (!appRecord.appIcon)
// Avoid the app icon download if the app already has an icon
{
[self startIconDownload:appRecord forIndexPath:indexPath];
}
}
}
}
这里快速枚举可见cell的indexpath,并根据对应AppIcon的地址下载。
// -------------------------------------------------------------------------------
// startIconDownload:forIndexPath:
// -------------------------------------------------------------------------------
- (void)startIconDownload:(AppRecord *)appRecord forIndexPath:(NSIndexPath *)indexPath
{
IconDownloader *iconDownloader = [self.imageDownloadsInProgress objectForKey:indexPath];
if (iconDownloader == nil)
{
iconDownloader = [[IconDownloader alloc] init];
iconDownloader.appRecord = appRecord;
[iconDownloader setCompletionHandler:^{
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
// Display the newly loaded image
cell.imageView.image = appRecord.appIcon;
// Remove the IconDownloader from the in progress list.
// This will result in it being deallocated.
[self.imageDownloadsInProgress removeObjectForKey:indexPath];
}];
[self.imageDownloadsInProgress setObject:iconDownloader forKey:indexPath];
[iconDownloader startDownload];
}
}
这里创建一个下载器,下载器提供了图片下载和对于尺寸不符合的图片进行重新绘制的支持,这里需要指定下载器的完成block,当然是将对应cell的image设置为下载的图片,然后从管理下载器的字典中删除该下载器。设置完下载器后,将其放入管理字典中,这里主要是因为下载器指针是本地变量,需要放到全局的集合中进行统一管理。
我们忘记讲什么了?
对的,忘记讲parse operation了。ParseOperation继承自NSOperation,在这里负责解析工作。你可以把它看做是一个新线程,任何线程的入口都是main,所以你需要在main里创建一个parser:
- (void)main
{
self.workingArray = [NSMutableArray array];
self.workingPropertyString = [NSMutableString string];
// It's also possible to have NSXMLParser download the data, by passing it a URL, but this is not
// desirable because it gives less control over the network, particularly in responding to
// connection errors.
//
NSXMLParser *parser = [[NSXMLParser alloc] initWithData:self.dataToParse];
[parser setDelegate:self];
[parser parse];
if (![self isCancelled])
{
// Set appRecordList to the result of our parsing
self.appRecordList = [NSArray arrayWithArray:self.workingArray];
}
self.workingArray = nil;
self.workingPropertyString = nil;
self.dataToParse = nil;
}
当然如何解析不是我们的重点,就不再赘述了。
结束
当然我们还可以定制别的懒加载策略,因为滑动过快,用户可能还没仔细看前面的AppIcon,因为我们也可以让前面出现过的图片作为占位图:
if(indexPath.row <= 7)
cell.imageView.image = [UIImage imageNamed:@"Placeholder.png"];
这样就只会在前面八个cell中使用预置的占位图,后面出现的会复用原来的图,然后再更新,当然这样会有一点遗留问题,如果预置占位图的cell太多,在刚开始下滑时候,使用预置占位图的cell会出现,而不是立即使用复用的cell的图;如果太少,就会出现没有AppIcon,效果也不行,还需要进一步优化。
这里我们就使用较为折衷的办法,仅当来不及使用复用的图片又没有下载完图片时,使用预置占位图,经测试,只会出现一次。
if(!cell.imageView.image)
cell.imageView.image = [UIImage imageNamed:@"Placeholder.png"];