您的位置:首页 > 移动开发 > IOS开发

如何在iOS上展现Web Service数据

2013-04-03 14:50 218 查看
在iOS开发中,需要和WEB服务器进行交互,如将一批来自WEB SERVICE的数据展现在表格上。数据交互格式是XML,使用的协议是SOAP。请求的数据中有图片,通常图片都会是一个URL重连接,需要再得到这个URL后下载到终端才展现出来。

如果你使用的是浏览器,那么这一切它都做好了。但如果你要更灵活的展现和处理这些数据,这需要开发一个应用。

1.实现过程

我建立一个简单的基于视图控制器的应用。新建的视图控制器类XYViewController。

在该类中手工添加UITableView对象resultTableView,用于展现WEB Service中请求来的数据。WEB SERVICE使用SOAP协议交互。建一个数据请求类XYQueryHotel,使用它的delegate将数据以数组的形式回调回来。

在这个数据请求类中,使用异步请求数据,将收到的XML格式的数据使用NSXMLParser类进行分析。

在视图控制器类XYViewController请求数据过程中,不可避免地会有一个等待出现,但UI可以继续,因为是异步请求操作。这个上面可以设置一些用于杀时间的有趣味的小图片,避免枯燥的等待,提升UI友好度。

在数据请求类操作完成后,通过delegate方式返回了数据给视图控制器类XYViewController中一个属性resultHotels。视图控制器类XYViewController将该属性的数据展现在UITableView对象resultTableView中。

对于resultTableView,通过的datasource方法(UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath,将能显示在屏幕上的cell设置好数据。

在resultTableView中,还有一个图片信息需要展现,这个需要通过resultHotels中图片URL去二次请求web service。

这个过程也需要异步去实现,包括请求到图片uiimage数据,请求到的数据的优化,请求到的数据展现等操作,反正不能影响到UI。这是整个实现过程中的关键点,也是难点。

这个请求操作是数据一开始加载时就发出。(UITableViewCell *)tableView:(UITableView *)tableViewcellForRowAtIndexPath:(NSIndexPath *)indexPath,这个方法中在cell填充时使用多线程发出请求。它的实现代码如下:

-(UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath {

static NSString *CellIdentifier =@"resultCell";

//初始化cell并指定其类型,也可自定义cell

hotelCell = (XYHotelCell*)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];

if(hotelCell == nil)

{

//将Custom.xib中的所有对象载入

NSArray *nib = [[NSBundle mainBundle]loadNibNamed:@"KaiFangTableViewCell" owner:nil options:nil];

//第一个对象就是CustomCell了

hotelCell = [nib objectAtIndex:0];

}

switch (indexPath.section) {

case 0://对应各自的分区

//修改CustomCell的控件

hotelInfo=[resultHotelsobjectAtIndex:indexPath.row];

hotelCell.name.text = [hotelInfoname];

hotelCell.addr.text = [hotelInfoaddr];

hotelCell.distance.text = [NSStringstringWithFormat:@"%@,%@",[hotelInfo lat],[hotelInfo lng]];

if(hotelInfo.hasLogoImage)

{

hotelCell.logo.image=hotelInfo.logo;

}

else

{

hotelCell.logo.image = [UIImageimageNamed:@"Placeholder.png"];

if (!resultTableView.dragging&& !resultTableView.decelerating) {

[selfstartOperationsForHotelLogo:hotelInfo atIndexPath:indexPath];

}

}

//返回CustomCell

return hotelCell;

break;

}

return hotelCell;//返回cell

}

数组resultHotels中保存类HotelInfo的对象的记录。类HotelInfo是一个单独定义的数据类,属于MVC中MODE范畴。

方法tableView:cellForRowAtIndexPath:,根据在屏幕上显示的cell行号,得到该数组中去动态取对应行的记录。然后将得到的记录信息填写到cell中,用于展现,并将得到的记录,通过一个方法 [self startOperationsForHotelLogo:hotelInfoatIndexPath:indexPath];去多线程请求展现图片。

这个方法 [self startOperationsForHotelLogo:hotelInfoatIndexPath:indexPath]中包括了图片从WEB SERVICE上下载操作,图片美化操作,图片展示到对应的CELL中的操作,并修改提交的记录中信息,再二次展现时如上下滚动时不需要再二次请求该方法了。

如果用户退出这个视图控制器类,那么该数组resultHotels如何缓存,怎不能再将WEB SERVICE交互再做一次把。

继续实现[selfstartOperationsForHotelLogo:hotelInfo atIndexPath:indexPath]方法,

-(void)startOperationsForHotelLogo:(HotelInfo *)hotel atIndexPath:(NSIndexPath*)indexPath {

if (!hotel.hasLogoImage) {

[selfstartImageDownloadingForHotelLogo:hotel atIndexPath:indexPath];

}

if (!record.isFiltered) {

[self startImageFiltrationForRecord:recordatIndexPath:indexPath];

}

}

根据传入的hotel对象中hasLogoImage来决定是否下载,再根据isFiltered来决定是否美化它。

继续实现(void)startImageDownloadingForHotelLogo:(HotelInfo*)hotel atIndexPath:(NSIndexPath *)indexPathWithLogo方法。

-(void)startImageDownloadingForHotelLogo:(HotelInfo *)hotelatIndexPath:(NSIndexPath *)indexPathWithLogo {

if (self.pendingOperations==nil)

{

self.pendingOperations=[[PendingOperationsalloc] init];

NSLog(@"pendingOperationsinitial");

}

if(![self.pendingOperations.downloadsInProgress.allKeys containsObject:indexPathWithLogo]){

HotelImageDownloader *imageDownloader = [[HotelImageDownloader alloc]initWithHotel:hotel atIndexPath:indexPathWithLogo delegate:self];

[self.pendingOperations.downloadsInProgress setObject:imageDownloaderforKey:indexPathWithLogo];

[self.pendingOperations.downloadQueueaddOperation:imageDownloader];

NSLog(@"operation count is%d",[self.pendingOperations.downloadQueue operationCount]);

}

}

首先,分配并初始化一个对象pendingOperations,用于保存操作队列的NSMutableDictionary对象和操作的NSOperationQueue对象。前者保存了哪些操作在操作队列中,然后多线程运行该操作。如果多线程操作取消了,那么也需要在操作队列中删除该操作。

接着,判断这个indexPath对应的记录在不在下载队列中,如果在,就不再二次操作了。如果不在,执行下载操作。

而这个下载操作的实现是一个非常关键的点。它需要多线程实现,并在完成后反馈到主线程中。多线程的实现方法有很多种,就我知道的有三种方式,分别是NSThread,NSOperation,Grand Central Dispatch (GCD)。

GCD的实现方法用来实现比较简单的逻辑,我不知道怎么调用delegate。

dispatch_queue_timageQueue=dispatch_queue_create("hotelInfo.logo.imageQueue", NULL);

dispatch_async(imageQueue, ^{

NSData *imageData = [NSDatadataWithContentsOfURL:[hotel logoURL]];

dispatch_async(dispatch_get_main_queue(),^{

[hotelInfo setLogo:[UIImageimageWithData:imageData]];

[resultTableView reloadRowsAtIndexPaths:[NSArrayarrayWithObject:indexPathWithLogo]withRowAnimation:UITableViewRowAnimationNone]; });

});

NSOperation是在GCD基础上封装,使用更简单些。如果实现简单的逻辑,只需要用block;如果实现复杂的逻辑,也只需要以NSOperation为父类,重写main()方法即可。 NSOperation是一个抽象类。(我现在也不明白啥叫抽象类)

NSThread的实现方法,我没用过,不介绍了,自己gogole把。

在这里使用NSOperation的子类HotelImageDownloader来实现下载操作,再将这个HotelImageDownloader类定义对象加到NSOperationQueue中,实现后台下载操作。

将hotelInfo,indexPath和delegate传到该对象中去,这些是需要实现的操作的必备条件,并使用delegate回调到主线程,以更新UI。

HotelImageDownloader*imageDownloader = [[HotelImageDownloader alloc] initWithHotel:hotelatIndexPath:indexPathWithLogo delegate:self];

HotelImageDownloader的主要函数定义如下:

#pragmamark -

#pragmamark - Life Cycle

-(id)initWithHotel:(HotelInfo *)hotel atIndexPath:(NSIndexPath *)indexPathdelegate:(id<HotelImageDownloaderDelegate>)theDelegate

{

if (self = [super init]) {

// Set the properties.

self.delegate = theDelegate;

self.indexPathInTableView = indexPath;

self.hotelInfo = hotel;

}

return self;

}

#pragmamark -

#pragmamark - Downloading image

// 3:Regularly check for isCancelled, to make sure the operation terminates as soonas possible.

-(void)main {

// 4: Apple recommends using@autoreleasepool block instead of alloc and init NSAutoreleasePool, becauseblocks are more efficient. You might use NSAuoreleasePool instead and thatwould be fine.

if (self.isCancelled)

return;

NSData *imageData = [[NSData alloc]initWithContentsOfURL:self.hotelInfo.logoURL];

if (self.isCancelled) {

imageData = nil;

return;

}

if (imageData) {

UIImage *downloadedImage = [UIImageimageWithData:imageData];

self.hotelInfo.logo = downloadedImage;

}

else {

self.hotelInfo.failed = YES;

}

imageData = nil;

if (self.isCancelled)

return;

// NSLog(@"performSelectorOnMainThread");

// 5: Cast the operation to NSObject, andnotify the caller on the main thread.

[(NSObject *)self.delegateperformSelectorOnMainThread:@selector(logoImageDownloaderDidFinish:)withObject:self waitUntilDone:NO];

}

代码[(NSObject*)self.delegate performSelectorOnMainThread:@selector(logoImageDownloaderDidFinish:)withObject:self waitUntilDone:NO];就是实现到主线程的回调,将HotelImageDownloader类的对象imageDownloader通过参数项"withObject:self"传回去,self就是imageDownloader对象,它的属性有hotelInfo信息和indexPath信息。通过这些信息实现WEB
SERVICE的请求结果集hotelResults的更新和UITABLEVIEW对应的cell的更新(也就是更新uiimage)。

这个操作是使用delegate实现的,也有人用NSNotificationCenter实现。

它的大致实现过程是这样.

首先,在视图控制器类XYViewController中的viewDidLoad方法中增加一个通知。名称为@"hotelLogoDownloader.completed"。该通知在触发时,会调用一个方法"logoImageDownloaderDidFinish:"。这个方法就是委托中需要实现的方法。

[[NSNotificationCenterdefaultCenter] addObserver:selfselector:@selector(logoImageDownloaderDidFinish:)name:@"hotelLogoDownloader.completed" object:nil];

其次,在HotelImageDownloader类中main()方法中,添加一个发送通知的代码,和这里调用delegate操作异曲同工。

NSDictionary*userInfo=[NSDictionary dictionaryWithObjectsAndKeys:[hotelobjectForKey:@"code"],@"code" ,nil];

[[NSNotificationCenterdefaultCenter] postNotificationName:@"hotelLogoDownloader.completed"object:self userInfo:userInfo];

通知提交操作的参数项"object:self"传回去,self就是imageDownloader对象,它的属性有hotelInfo信息和indexPath信息。

这样也实现了消息传递机制。

两个方法都是为了将消息传递到主线程,属于跨线程,跨方法的消息传递操作。

不管使用何种方式,在主线程的视图控制器类XYViewController中,都要实现方法"logoImageDownloaderDidFinish:"的定义。

-(void)logoImageDownloaderDidFinish:(HotelImageDownloader *)downloader {

NSLog(@"logoImageDownloaderDidFinishis executed");

NSIndexPath *indexPath =downloader.indexPathInTableView;

HotelInfo *theHotel = downloader.hotelInfo;

[hotelResultsreplaceObjectAtIndex:indexPath.row withObject:theHotel];

[resultTableView reloadRowsAtIndexPaths:[NSArrayarrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];

[self.pendingOperations.downloadsInProgressremoveObjectForKey:indexPath];

}

这个方法就是用来实现WEB SERVICE的请求结果集hotelResults的更新和UITABLEVIEW对应的cell的更新(也就是更新uiimage),并且在最后结束将下载队列字典中该键和值删除。

该逻辑的实现代码是最后三行。

代码[hotelResultsreplaceObjectAtIndex:indexPath.row withObject:theHotel];实现了根据行号更新数组hotelResults的对应行的记录。

代码[resultTableView reloadRowsAtIndexPaths:[NSArrayarrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];实现uitableview的datasource方法的调用。

这一方法会重新加载所指定indexPaths中的UITableViewCell实例,因为重新加载cell所以会请求这个UITableView实例的data source来获取新的cell;这个表会用动画效果让新的cell进入,并让旧的cell退出。会调用UITableViewDataSource协议中的所有方法来更新数据源,其中调用 (UITableViewCell *)tableView:(UITableView *)tableViewcellForRowAtIndexPath:(NSIndexPath
*)indexPath ,只会调用所需更新的行数,来获取新的cell。此时该cell的-(void)setSelected:(BOOL)selected animated:(BOOL)animated将被调用,所设置的selected为NO;

只是,不知道是只更新一个屏幕中正显示的cell还是会更新屏幕中正显示的所有的cell。

如果屏幕上将hotelResults的所有的记录一屏都显示完了,那么事情到这里就结果了。但是,很遗憾,这不是PC终端,没有那么大的屏幕。就是PC终端,也有一个lazy load功能,没那么啥一下子请求所有的数据,也是一屏幕满了,你下拉才会显示剩下的内容。这是基于性能考虑的。 在iphone终端上,这种需求更强烈。应用不可能将hotelResults所有记录都先写到UI内存中,而是屏幕显示多少行,显示那些行,就写入到屏幕内存中。

于是,新的问题就来了。

因为iphone屏幕会上下滚动,这是uitableview的最基本的功能。上下滚动操作过程中,屏幕上显示的hotelResults记录会变化。

如初始显示的是1-10行记录,下拉后显示的5-15行。 那么,前5行记录的logoImg信息,我们就不要再下载了,原来的下载操作队列需要取消,再增加11-15录的下载操作。如果下载了前行记录,再reloadRowsAtIndexPaths:withRowAnimation:操作时,那么显示操作就失败了,很明显没有办法更新屏幕了。

这个滚动操作,有uiscrollview的delegate实现,我们需要做的是将delegate的方法在主视图控制器类XYViewController中实现一下。

#pragmamark -

#pragmamark - UIScrollView delegate

-(void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {

[self.pendingOperations.downloadQueuesetSuspended:YES];

}

-(void)scrollViewDidEndDragging:(UIScrollView *)scrollViewwillDecelerate:(BOOL)decelerate {

if (!decelerate) {

[self loadImagesForOnscreenCells];

[self.pendingOperations.downloadQueuesetSuspended:NO];

}

}

-(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {

// 3: This delegate method tells you thattable view stopped scrolling, so you will do the same as in #2.

NSLog(@"scrollViewDidEndDecelerating");

[self loadImagesForOnscreenCells];

//[downloadLogoQueue setSuspended:NO];

[self.pendingOperations.downloadQueuesetSuspended:NO];

}

#pragmamark -

#pragmamark - Cancelling, suspending, resuming queues / operations

-(void)loadImagesForOnscreenCells {

NSSet *visibleRows = [NSSetsetWithArray:[resultTableView indexPathsForVisibleRows]];//现在展现在屏幕上的如6-15行

NSMutableSet *pendingOperations =[NSMutableSet setWithArray:[self.pendingOperations.downloadsInProgressallKeys]];//正在下载队列的1-8行。

NSMutableSet *toBeCancelled =[pendingOperations mutableCopy];

NSMutableSet *toBeStarted = [visibleRowsmutableCopy];

[toBeStarted minusSet:pendingOperations];//现在展现在屏幕上的如6-15行,减去,正在下载队列的1-8行。得到需要加到下载队列的行。

[toBeCancelled minusSet:visibleRows];////正在下载队列的1-8行 ,减去,现在展现在屏幕上的如6-15行。得到需要取消下载队列的行。

for (NSIndexPath *anIndexPath intoBeCancelled) {

HotelImageDownloader *pendingDownload =[self.pendingOperations.downloadsInProgress objectForKey:anIndexPath];

[pendingDownload cancel];//就是调用[NSOperationcancel]; 取消该operation操作。

[self.pendingOperations.downloadsInProgressremoveObjectForKey:anIndexPath];

}

toBeCancelled = nil;

for (NSIndexPath *anIndexPath intoBeStarted) {

HotelInfo *hotelToProcess = [hotelInfosobjectAtIndex:anIndexPath.row];

[selfstartOperationsForHotelLogo:hotelToProcess atIndexPath:anIndexPath];//执行多线程下载,美化等用于图片展现的操作。

}

toBeStarted = nil;

}

方法[selfstartOperationsForHotelLogo:hotelToProcess atIndexPath:anIndexPath]也是在” (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath“方法中调用的。

视图滚动操作会刷新屏幕,也就是为调用”tableView:cellForRowAtIndexPath:“。那么,这里的bestarted调用是不是有点多余了,因为在tableView:cellForRowAtIndexPath:方法中,不是会自动调用"startOperationsForHotelLogo:atIndexPath:"方法吗?虽然这里调用了,在后来的也会因为(![self.pendingOperations.downloadsInProgress.allKeyscontainsObject:indexPathWithLogo])这个条件判断而不会加载到下载队列中去。但毕竟属于二次调用了。

2.总结

在这个逻辑实现过程中,涉及到众多知识点。其中delegate,uitableview,nsoperation,nsoperationqueue,nsxmlparser,NSNotificationCenter,也数组的深度复制,字典的处理等,

本文参考了http://www.raywenderlich.com/19788/how-to-use-nsoperations-and-nsoperationqueues
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: