iCPVanity:iOS 7 的 CP Vanity






4.67/5 (5投票s)
CP Vanity 的 iOS 移植版本
Content
引言
为了给自己设定一个学习 iOS 编程的目标,我决定将Windows Phone 上的 CPVanity 应用移植到 iOS 7 平台。我从中学习了很多,希望通过写这篇文章,你也能学到一些东西。如果不行,至少你也有了一个漂亮的 iPhone 应用。
你可以在Youtube上看到它的实际运行效果。
你能学到什么(如果你还不知道的话)
- iOS 中的正则表达式
- 解析XML
- 使用 Storyboard 进行导航
- 导航过程中数据的来回传递
那么,废话不多说……
获取数据
我将 CodeProject 网站抽象成了一些类
SDECodeProjectUrlScheme
:一个类,用于获取下载数据的各种 URLSDECodeProjectArticle
:一个类,抽象了文章的主要属性,如标题、描述、链接等。SDECodeProjectMember
:一个类,抽象了用户信息检索,如姓名、声望、文章数、博客数等。SDECodeProjectFeed
:一个类,抽象了 CodeProject Feed 的定义:它的名称以及下载 Feed 的 URL。
获取用户信息: SDECodeProjectMember
选定用户的数据是通过正则表达式从网页上抓取的。这个抓取过程包括下载内容和提取所需的数据项。
下载数据
数据下载是通过 NSURLConnection
类在代码中完成的。它会异步地提供一个 URL 来获取数据。你需要提供一个实现两个方法的代理
didReceiveData
:在有数据可用时(重复地)被调用。将收到的数据追加到之前收到的数据中。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 Pattyn的CPVanity,但我并没有严格遵循其实现。
首先,我不确定是否可能,因为底层模式差异很大,iOS 遵循 MVC 结构,并且不提供原生的数据绑定支持。
其次,每个平台都有其用户界面设计的范例。iOS 用户不期望应用程序的行为像 Windows Phone 或 Android 应用程序,因此没有必要强加给他们。
版本历史
- 版本 1.0:初始版本