Click here to Skip to main content
15,868,016 members
Articles / Mobile Apps / iPhone

iCPVanity: CP Vanity for iOS 7

Rate me:
Please Sign up or sign in to vote.
4.67/5 (5 votes)
12 Mar 2014CPOL5 min read 20.9K   72   3  
A port of CP Vanity to iOS

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):

  1. Regular expressions in iOS
  2. Parsing XML
  3. Navigation with storyboards
  4. 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 data
  • SDECodeProjectArticle: 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:

  1. didReceiveData which gets (repeatedly) called when some data is available. Append the received data to previously received data.
  2. connectionDidFinishLoading which is called to signal all data was loaded. It is now time to process the data.

The SDECodeProjectMember implements this delegate:

Objective-C
- (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
}

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:

Objective-C
- (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:

Objective-C
// 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);

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:

Objective-C
- (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:

Objective-C
- (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.

Objective-C
@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.

Objective-C
@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.

Objective-C
@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.

Objective-C
@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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
Belgium Belgium
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
-- There are no messages in this forum --