如何在iOS上展现Web Service数据

来源:互联网 发布:淘宝偷换链接怎么处理 编辑:程序博客网 时间:2024/06/10 04:28

在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。
原创粉丝点击