Content
Introduction
In setting myself a goal for learning iOS programming, I decided to port the CPVanity windows phone application to the iOS 7 platform. I learned a lot from it and I hope that by writing this article, you learn something to. And if not, well you have a nice application for your iPhone.
You can see it in action on Youtube.
What can you learn (if you don't know it already):
- Regular expressions in iOS
- Parsing XML
- Navigation with storyboards
- Passing parameters back and forth during navigation
So, without any further ado...
Getting the Data
I've abstracted the CodeProject site in a few classes:
SDECodeProjectUrlScheme
: A class which enables you to get the various URLs used to download the dataSDECodeProjectArticle
: A class abstracting the main properties of an article, like the title, description, link, etc.SDECodeProjectMember
: A class abstracting the retrieval of the member data, like his name, reputation, number of articles, blogposts, etc. SDECodeProjectFeed
: A class abstracting a CodeProject feed definition: its name and the URL to download the feed.
Getting the User Data: SDECodeProjectMember
The data of selected user is scraped from the webpages by using regular expressions. This scraping involves downloading the content and capturing the needed data items.
Downloading the Data
Downloading the content is done in code by using the NSURLConnection
class. It asynchronously gets the data provided an URL. You have to provide it a delegate which must implement two methods:
didReceiveData
which gets (repeatedly) called when some data is available. Append the received data to previously received data.connectionDidFinishLoading
which is called to signal all data was loaded. It is now time to process the data.
The SDECodeProjectMember
implements this delegate:
- (id)initWithId:(int)memberId delegate:(id<SDECodeProjectMemberDelegate>)delegate
{
profilePageData = [NSMutableData new];
profilePageConnection =[NSURLConnection connectionWithRequest:
[NSURLRequest requestWithURL:
[NSURL URLWithString:memberProfilePageUrl]]
delegate:self];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
if(connection == profilePageConnection)
[profilePageData appendData:data];
}
- (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];
}
}
The members data is spread over two pages, that is why you will find two of these constructs in the accompanying code.
Capturing the Data
The needed data items are captured using regular expressions. This is not meant to be an article on regular expressions so I will not explain them in detail. Just download the code and study the expressions used.
My code is somewhat different here in intent than in the original article: I'm making intensive use of the capturing capability of regular expressions where the original .NET code just gets a part of the text and then uses substr
like constructs to get the data items.
Regular expressions are implemented by the NSRegularExpression
class. As a sample, I will show you how the members name is extracted.
Because regular expressions are used so intensively in this application, I've abstracted their usage in two methods:
- (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;
}
Following is an example of their use:
NSString* avgArticleRatingMatchingPattern = @"average article rating: ([0-9.]*)";
self.AvgArticleRating = [self captureForPattern: avgArticleRatingMatchingPattern inText:page];
NSLog(@"AvgArticleRating: %@", self.AvgArticleRating);
Getting the RSS feeds: SDERSSFeed
Getting at an RSS feed also involves two steps similar to getting the member data: first downloading the feed and next parsing the XML. Fortunately, iOS gives us a class incorporating the two steps: NSXMLParser
Parsing the RSS Feed
Parsing XML using the above class also requires a delegate implementing the following methods:
didStartElement
: Called when an XML node is started didEndElement
: Called when an XML node ended foundCharacters
: Text between two nodes parserDidEndDocument
: Called when the complete document was processed
Parsing an RSS feed using these methods is done like the following:
- (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];
}
}
Visualizing the Data
Navigation Overview
The application starts with a tabbed view initialized on the member screen. The other tabs allow you to navigate to the two RSS feed visualization screens: one for the article feeds and another one for the forum feeds.
From the member screen, you can navigate to the memberarticles screen and from there to the member reputation screen.
From the RSS feed screens, you can navigate to screens allowing you to select the RSS you want to see:
Passing Data Back and Forth
Passing Data Forward: From Calling to Callee
While switching from one screen to the other using storyboards, you are not responsible for instantiating the target screen. The navigation is defined in the storyboard by means of a UIStoryboardSegue
.
When executing the segue, the method prepareForSegue:sender:
of the navigationsource
screen is called with as an argument the segue which is about to be executed. And this segue contains the instance of the target screen to which you will navigate. So this is the moment to pass any data to the navigation target.
Of course, it is possible to navigate to different target screens from one source screen. For this, you give a name to the segue so in the method prepareForsegue:sender:
you check the name of the segue passed in.
In code, this looks like the following:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([segue.identifier isEqualToString:@"MemberArticlesSegue"]) {
SDECPUserArticlesViewController *memberArticlesViewController =
(SDECPUserArticlesViewController*)segue.destinationViewController;
memberArticlesViewController.CodeprojectMember = codeprojectMember;
}
}
Passing Data Backward: Back from Callee to Caller
Passing data back to the caller is done through a concept called delegates, which has nothing to do with the .NET concept of delegates.
First, define a protocol. The protocol will have methods which allow the callee to pass data back to the caller.
@protocol SDECPRssFeedSelection <NSObject>
- (void) selectedFeed:(SDECodeProjectFeed*) feed;
@end
Second, let the caller implement the protocol. The implementation will store the data supplied by the object calling the protocol implementation.
@interface SDECPRssViewController : UIViewController<SDECPRssFeedSelection, SDERSSFeedDelegate, UITableViewDataSource, UITableViewDelegate>
- (void) selectedFeed:(SDECodeProjectFeed*) feed;
@end
@implementation SDECPRssViewController
- (void) selectedFeed:(SDECodeProjectFeed*) feed
{
self.Feed = feed;
}
@end
Third: hand over this prototcol to the callee, giving it a means to hand back any data.
@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
Fourth and finally, in the callee, invoke the appropriate method on the provided protocol implementation to hand back the appropriate data.
@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
Conclusion
Although inspiration for this app came from the Windows Phone version of Luc Pattyn's CPVanity, I did not follow that implementation in great detail.
First of all, I am not sure that is even possible as the underlying patterns are quite different with iOS following an MVC structure and providing no native support for databinding.
Secondly, every platform has its own paradigms for user interface design. And an iOS user does not expect an application to behave as a Windowed Phone or Android application, so there is no need to force it upon him or her.
Version History
- Version 1.0: Initial version