65.9K
CodeProject 正在变化。 阅读更多。
Home

iCPVanity:iOS 7 的 CP Vanity

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.67/5 (5投票s)

2014年3月11日

CPOL

5分钟阅读

viewsIcon

22126

downloadIcon

112

CP Vanity 的 iOS 移植版本

Content

引言

为了给自己设定一个学习 iOS 编程的目标,我决定将Windows Phone 上的 CPVanity 应用移植到 iOS 7 平台。我从中学习了很多,希望通过写这篇文章,你也能学到一些东西。如果不行,至少你也有了一个漂亮的 iPhone 应用。

你可以在Youtube上看到它的实际运行效果。

你能学到什么(如果你还不知道的话)

  1. iOS 中的正则表达式
  2. 解析XML
  3. 使用 Storyboard 进行导航
  4. 导航过程中数据的来回传递

那么,废话不多说……

获取数据

我将 CodeProject 网站抽象成了一些类

  • SDECodeProjectUrlScheme:一个类,用于获取下载数据的各种 URL
  • SDECodeProjectArticle:一个类,抽象了文章的主要属性,如标题、描述、链接等。
  • SDECodeProjectMember:一个类,抽象了用户信息检索,如姓名、声望、文章数、博客数等。
  • SDECodeProjectFeed:一个类,抽象了 CodeProject Feed 的定义:它的名称以及下载 Feed 的 URL。

获取用户信息: SDECodeProjectMember

选定用户的数据是通过正则表达式从网页上抓取的。这个抓取过程包括下载内容和提取所需的数据项。

下载数据

数据下载是通过 NSURLConnection 类在代码中完成的。它会异步地提供一个 URL 来获取数据。你需要提供一个实现两个方法的代理

  1. didReceiveData:在有数据可用时(重复地)被调用。将收到的数据追加到之前收到的数据中。
  2. connectionDidFinishLoading:在所有数据加载完毕时被调用,表明是时候处理数据了。

SDECodeProjectMember 类实现了这个代理。

- (id)initWithId:(int)memberId delegate:(id<SDECodeProjectMemberDelegate>)delegate
{
    // more code
        profilePageData = [NSMutableData new];
        profilePageConnection =[NSURLConnection connectionWithRequest:
                                [NSURLRequest requestWithURL:
                                 [NSURL URLWithString:memberProfilePageUrl]]
                                                             delegate:self];
    // more code
}

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    if(connection == profilePageConnection)
        [profilePageData appendData:data];
        
    // more code
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
    if(connection == profilePageConnection)
    {
        NSString *profilePage = [[NSString alloc]initWithData:profilePageData 
            encoding:NSASCIIStringEncoding];
        
        [self fillMemberProfileFromProfilePage:profilePage];
        
        self.ProfilePageLoaded = true;

        if(self.delegate != NULL)
            [self.delegate codeprojectMemberProfileAvailable];
    }
    
    // more code
}

成员数据分布在两个页面上,因此在附带的代码中你会找到两个这样的结构。

提取数据

所需的数据项是使用正则表达式提取的。本文无意详细讲解正则表达式,因此请下载代码自行研究其中使用的表达式。

我的代码在这里的意图与原始文章略有不同:我大量使用了正则表达式的捕获功能,而原始的 .NET 代码只是获取一部分文本,然后使用 substr 类的构造函数来获取数据项。

正则表达式是通过 NSRegularExpression 类实现的。作为示例,我将展示如何提取成员姓名。

由于此应用中大量使用了正则表达式,我已经将它们的用法抽象成了两个方法。

- (NSArray*)matchesForPattern:(NSString*)pattern inText:(NSString*)text {
    NSError *error = NULL;
    NSRegularExpression *regex = [NSRegularExpression
                                             regularExpressionWithPattern:pattern
                                             options:NSRegularExpressionCaseInsensitive
                                             error:&error];
    if (error)
    {
        NSLog(@"Couldn't create regex with given string and options");
    }
    
    NSRange matchRange = NSMakeRange(0, text.length);
    return [regex matchesInString:text options:0 range:matchRange];
}

- (NSString*)captureForPattern:(NSString*)pattern inText:(NSString*)text {
    NSString *captureString = @"Error";
    NSArray *matches = [self matchesForPattern: pattern inText:text];
    if(matches.count != 0)
    {
        NSTextCheckingResult* firstMatch = [matches firstObject];
        NSRange matchRange = [firstMatch rangeAtIndex:1];
        captureString = [text substringWithRange:matchRange];
    }
    
    return captureString;
}

下面是使用示例

    // Average article rating: 4.66
    // average article rating: ([0-9./]*)
    NSString* avgArticleRatingMatchingPattern = @"average article rating: ([0-9.]*)";
    self.AvgArticleRating = [self captureForPattern: avgArticleRatingMatchingPattern inText:page];
    NSLog(@"AvgArticleRating: %@", self.AvgArticleRating);

获取 RSS Feed: SDERSSFeed

获取 RSS Feed 也涉及两个步骤,与获取成员数据类似:首先下载 Feed,然后解析 XML。幸运的是,iOS 提供了一个包含这两个步骤的类:NSXMLParser

解析 RSS Feed

使用上述类解析 XML 也需要一个实现以下方法的代理:

  • didStartElement:在 XML 节点开始时调用
  • didEndElement:在 XML 节点结束时调用
  • foundCharacters:两个节点之间的文本
  • parserDidEndDocument:在整个文档处理完毕时调用

使用这些方法解析 RSS Feed 的方式如下:

- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName 
    namespaceURI:(NSString *)namespaceURI 
    qualifiedName:(NSString *)qName 
    attributes:(NSDictionary *)attributeDict 
{    
    element = elementName;
    
    if ([element isEqualToString:@"item"])
    {
        
        item = [[SDERSSItem alloc] init];
        title = [[NSMutableString alloc] init];
        description = [[NSMutableString alloc] init];
        link = [[NSMutableString alloc] init];
        author = [[NSMutableString alloc] init];
        pubDate = [[NSMutableString alloc] init];
    }    
}

- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName 
    namespaceURI:(NSString *)namespaceURI 
    qualifiedName:(NSString *)qName 
{    
    if ([elementName isEqualToString:@"item"])
    {        
        item.Title = title;
        
        NSRange r;
        while ((r = [description rangeOfString:@"<[^>]+>" options:NSRegularExpressionSearch]).location != NSNotFound)
            description = [description stringByReplacingCharactersInRange:r withString:@""];
        
        item.Description = [description gtm_stringByUnescapingFromHTML];
        
        item.Link = link;
        item.Author = author;
        item.Date = pubDate;
        
        if(result == nil)
        {
            result = [[NSMutableArray alloc] init];
        }
        [result addObject:item];        
    }
    else if([elementName isEqualToString:@"title"] && [element isEqualToString:@"title"])
    {
        element = @"";
    }
    else if([elementName isEqualToString:@"description"] && [element isEqualToString:@"description"])
    {
        element = @"";
    }
    else if([elementName isEqualToString:@"link"] && [element isEqualToString:@"link"])
    {
        element = @"";
    }
    else if([elementName isEqualToString:@"author"] && [element isEqualToString:@"author"])
    {
        element = @"";
    }
    else if([elementName isEqualToString:@"pubDate"] && [element isEqualToString:@"pubDate"])
    {
        element = @"";
    }    
}

- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string {
    
    if ([element isEqualToString:@"title"])
    {
        [title appendString:string];
    }
    else if ([element isEqualToString:@"description"])
    {
        [description appendString:string];
    }
    else if ([element isEqualToString:@"link"])
    {
        [link appendString:string];
    }
    else if ([element isEqualToString:@"author"])
    {
        [author appendString:string];
    }
    else if ([element isEqualToString:@"pubDate"])
    {
        [pubDate appendString:string];
    }    
}

可视化数据

导航概览

应用程序以一个标签视图开始,默认显示成员屏幕。其他标签允许你导航到两个 RSS Feed 的可视化屏幕:一个用于文章 Feed,另一个用于论坛 Feed。

从成员屏幕,你可以导航到成员文章屏幕,再到成员声望屏幕。

从 RSS Feed 屏幕,你可以导航到允许你选择要查看的 RSS 的屏幕。

数据前后传递

数据向前传递:从调用者到被调用者

在使用 Storyboard 在一个屏幕切换到另一个屏幕时,你不需要负责实例化目标屏幕。导航是通过 UIStoryboardSegue 在 Storyboard 中定义的。

执行 segue 时,navigation source 屏幕的 prepareForSegue:sender: 方法会被调用,参数是即将执行的 segue。这个 segue 包含了你将要导航到的目标屏幕的实例。所以这是将数据传递给导航目标的时刻。

当然,也可以从一个源屏幕导航到不同的目标屏幕。为此,你需要给 segue 命名,然后在 prepareForsegue:sender: 方法中检查传入的 segue 的名称。

代码中,这看起来如下:

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    if ([segue.identifier isEqualToString:@"MemberArticlesSegue"]) {
        
        SDECPUserArticlesViewController *memberArticlesViewController = 
            (SDECPUserArticlesViewController*)segue.destinationViewController;
        memberArticlesViewController.CodeprojectMember = codeprojectMember;        
    }
}

数据向后传递:从被调用者回到调用者

将数据传回调用者是通过一种称为委托的概念实现的,这与 .NET 的委托概念无关

第一步,定义一个协议。协议将包含允许被调用者将数据传回调用者的方法。

@protocol SDECPRssFeedSelection <NSObject>

- (void) selectedFeed:(SDECodeProjectFeed*) feed;

@end

第二步,让调用者实现该协议。实现将存储被调用对象通过协议实现提供的数据。

@interface SDECPRssViewController : UIViewController<SDECPRssFeedSelection, SDERSSFeedDelegate, UITableViewDataSource, UITableViewDelegate>

- (void) selectedFeed:(SDECodeProjectFeed*) feed;

@end

@implementation SDECPRssViewController

- (void) selectedFeed:(SDECodeProjectFeed*) feed
{
    self.Feed = feed;
}

@end

第三步:将这个协议交给被调用者,使其能够传回任何数据。

@interface SDECPArticleViewController : SDECPRssViewController

@end

@implementation SDECPArticleViewController

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    if ([segue.identifier isEqualToString:@"RSSArticleCategory"])
    {
        SDECPArticleCategoryViewController *categoryViewController = 
            (SDECPArticleCategoryViewController*)segue.destinationViewController;
        
        categoryViewController.categorySelectionDelegate = self;
    }
    else if([segue.identifier isEqualToString:@"RSSArticle"]) {
        SDECPPageViewController *pageViewController = 
            (SDECPPageViewController*)segue.destinationViewController;
        
        NSIndexPath *indexPath = [self.EntriesView indexPathForSelectedRow];
        
        pageViewController.Url = ((SDERSSItem*)[self.Entries objectAtIndex:indexPath.row]).Link;
    }
}

@end

第四步,也是最后一步,在被调用者中,调用协议实现上相应的方法来传回相应的数据。

@implementation SDECPArticleCategoryViewController

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    [tableView deselectRowAtIndexPath:indexPath animated:NO];
    [self.categorySelectionDelegate selectedFeed:[articleFeeds objectAtIndex:indexPath.row]];
    [[self navigationController] popViewControllerAnimated:YES];
}

@end

结论

尽管这款应用的灵感来自于Windows Phone 版Luc PattynCPVanity,但我并没有严格遵循其实现。

首先,我不确定是否可能,因为底层模式差异很大,iOS 遵循 MVC 结构,并且不提供原生的数据绑定支持。

其次,每个平台都有其用户界面设计的范例。iOS 用户不期望应用程序的行为像 Windows Phone 或 Android 应用程序,因此没有必要强加给他们。

版本历史

  • 版本 1.0:初始版本
© . All rights reserved.