Often in iPhone projects I’ve needed to parse XML documents from the internet.
- I want the data to be processed and shown to the user as quickly as possible.
- If the file is large I don’t want to have to store the entire contents in memory.
Using NSXMLParsers initWithURL: method the xml file is downloaded synchronously with NSURLConnection before parsing. If NSXMLParser was able to begin as soon as stream buffer began to fill, even though the overall parsing time wouldn’t be reduced significantly if the updates are immediately displayed the user perceived time decreases significantly.
Using the excellent objective c expat wrapper library by Robbie Hanson as a base iPhoneExpat uses CFNetwork & CFHTTPMessage to create and feed a http stream gradually into Expat. If the server supports gzip compression zlib is used to decompress the stream on fly.
iPhone Expat offer 3 methods for initialization
- (id)initWithContentsOfURL:(NSURL *)url;
- (id)initWithContentsOfFile:(NSString *)path;
- (id)initWithData:(NSData *)data;
The delegate messages are also a drop in replacement for NSXMLParser
@protocol ExpatXMLParserDelegate
@optional
- (void)parserDidStartDocument:(ExpatXMLParser*)parser;
- (void)parserDidEndDocument:(ExpatXMLParser*)parser;
- (void)parser:(ExpatXMLParser*)parser didStartMappingPrefix:(NSString *)prefix toURI:(NSString *)namespaceURI;
- (void)parser:(ExpatXMLParser*)parser didEndMappingPrefix:(NSString *)prefix;
- (void)parser:(ExpatXMLParser*)parser foundComment:(NSString *)comment;
- (void)parser:(ExpatXMLParser*)parser foundProcessingInstructionWithTarget:(NSString *)target data:(NSString *)data;
- (void)parser:(ExpatXMLParser*)parser parseErrorOccurred:(NSError *)parseError;
- (BOOL)parser:(ExpatXMLParser*)parser shouldProcessAttributesForElement:(NSString *)elementName;
@required
- (void)parser:(ExpatXMLParser*)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qualifiedName attributes:(NSDictionary *)attributeDict;
- (void)parser:(ExpatXMLParser*)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName;
- (void)parser:(ExpatXMLParser *)parser foundCharacters:(NSString *)string;
@end
However
- (void)parser:(ExpatXMLParser *)parser foundCharacters:(NSString *)string;
Will report the entire contents of a tag so you don’t need an NSMutablestring to buffer fragments in your delegate. e.g.
- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string
{
[buffer release];
buffer = string;
[buffer retain];
}
Should be good in 99% of cases to get the entire contents of a tag
The project can be found here
http://github.com/zootreeves/iPhoneExpat
Alternatively checkout the code using git: http://github.com/zootreeves/iPhoneExpat
Benchmark test output:
I’ve included a test application which sequentially downloads and parses the following feeds:
@"http://ax.itunes.apple.com/WebObjects/MZStoreServices.woa/ws/RSS/topalbums/sf=143441/limit=300/explicit=true/xml",
@"http://ax.itunes.apple.com/WebObjects/MZStoreServices.woa/ws/RSS/topalbums/sf=143441/limit=300/explicit=true/xml",
@"http://feeds.feedburner.com/DilbertDailyStrip",
@"http://designsponge.blogspot.com/atom.xml",
@"http://www.slate.com/rss/",
@"http://rssfeeds.usatoday.com/UsatodaycomBooks-TopStories",
@"http://googleblog.blogspot.com/atom.xml",
@"hhttp://api.flickr.com/services/feeds/groups_pool.gne?id=61057342@N00〈=en-us&format=rss_200",
@"http://phobos.apple.com/WebObjects/MZStore.woa/wpa/MRSS/topsongs/limit=25/rss.xml",
@"http://www.readwriteweb.com/rss.xml",
@"http://rssfeeds.usatoday.com/UsatodaycomNation-TopStories",
@"http://dictionary.reference.com/wordoftheday/wotd.rss",
@"http://www.quotationspage.com/data/qotd.rss",
@"http://sports.espn.go.com/espn/rss/news"
Time to reach the first element Expat: 8.611217 — NSXMLParser: 15.404100
Total time for Expat: 10.902272 — NSXMLParser: 17.108309
Memory is also reduced by a factor of about x4 (see screenshots)
Further Considerations
- It would be great if http requests were persistent, however after setting kCFStreamPropertyHTTPAttemptPersistentConnection to true using httpscoop I will still able to intercept “connection close” messages between requests to the same server. I think I must be doing something wrong here.
- It would be preferable if as many objects as possible were released manually rather than using an autorelease pool. For example the delegate methods that pass an elementName it is common for the client to use this string to determine their current tag, but rare that they actually want to retain it. The code could be altered so these strings are not placed in a pool and are released immediately after the delegate message is sent. Not only can the overhead of collection be avoided but CFStringCreateWithCharactersNoCopy can be used as well.
- With UTF8 creating CFStrings with the [NSString stringwithutf8string:] was no problem. However when I configured Expat to use UTF-16 characters internally I found that CFStringCreateWithCharacters would often return garbage. The problem is I was passing in UINT16_MAX as the buffer length hoping that the CFString would be terminated at null, however it doesn’t work that way and I had to change everything to calculate the buffer size using UniCharStrlen(buffer) before hand.
- FTP (CFFTPStream) and file streams could easily be supported.
JCJ is an iphone front end for the popular medical locums job agency jcjlocums.co.uk. One of the main requirements was that the app be fun and easy to use which allowed Chris Baxter to use his creativity and come up with a really unique design.
The animated intro was achieved using CoreAnimation to drop UIButtons from the top of the screen. The “multi value fields” used in the search screen are simply UILabels embedded in a UIScrollview using the paging feature to provide smooth scrolling between values. There are existing Calendar libraries for the iphone however I decided in the end to write a custom control which allows much tighter integration into the design.
JCJ Locums app is available free on the app store
Here is basic html parser which I’ve used in several projects recently. The code depends on libxml2 and is basically a thin wrapper that provides a more convenient interface for parsing html with objective c. This has only been tested on iphone OS 3.1.3 & 3.2, if your using a OSX your probably better of investigating using Webkit to manipulate your DOM.
The code below is provided under an MIT license, but if you do make any updates it would be great if you could send them back.
Usage:
//Example to download google's source and print out the urls of all the images
NSError * error = nil;
HTMLParser * parser = [[HTMLParser alloc] initWithContentsOfURL:[NSURL URLWithString:@"http://www.google.com"] error:&error];
if (error) {
NSLog(@"Error: %@", error);
return;
}
HTMLNode * bodyNode = [parser body]; //Find the body tag
NSArray * imageNodes = [bodyNode findChildTags:@"img"]; //Get all the
for (HTMLNode * imageNode in imageNodes) { //Loop through all the tags
NSLog(@"Found image with src: %@", [imageNode getAttributeNamed:@"src"]); //Echo the src=""
}
[parser release];
You can grab a copy of the code here on github:
http://github.com/zootreeves/Objective-C-HMTL-Parser
[email protected]:zootreeves/Objective-C-HMTL-Parser.git
Note:
-(NSString*)rawContents; does not work, you need to use NSString * rawContentsOfNode(xmlNode * node, htmlDocPtr doc); to dump the entire html contents of a node.
iElect UK is an iPhone application with a unique concept. During the upcoming 2010 election users will be able to use iElect to lookup their constituency and view information about their current MP and 2010 candidates. Users can then request that candidates gets in touch with them by sms or email.
Again this project was undertaken with the guys at Gedgers and engineroomapps. Core location and the Mapkit API are used to pinpoint the users location and find their default constituency. Core data is then used to store the list of constituencies and candidates details. The contact feature issues a request to the backend web service, which will forward the provided details onto the candidate. The striking orange buttons and rounded containers are drawn using Quartz 2D.
A fast and efficient C hash table implementation, which in many cases can beat google dense hash map, the STL Tr1 hash map and khash
The semi-interesting part is the way it handles collisions. When Sievehash encounters a collision is makes another hash table smaller than it’s parent. Each new key is dropped through the Seive until an empty spot is found and becomes it’s new home, forming a structure like a Seive.
Similar to most hash tables, when any table reaches the specified load factor all keys will be copied to a new larger table.
Sieve Hash Library: SieveHash.h
Hash test package: HashTests
*** Starting String Test: STL TR1
- Insert Took: 2.751855
- Lookup Took: 2.694130
} Test Took: 8.425282
*** Starting String Test: SieveHash
- Insert Took: 4.447270
- Lookup Took: 2.972444
} Test Took: 10.369791
*** Starting String Test: KHash CPP
- Insert Took: 2.676008
- Lookup Took: 3.056431
} Test Took: 8.704912
*** Starting String Test: Google Dense Hash Test
- Insert Took: 5.109226
- Lookup Took: 4.746739
} Test Took: 12.817609
WARNING: This is not meant for production code, it is for testing purposes only.
MyStacks is an iPhone application for browsing questions from the stack overflow trilogy sites, those are stackoverflow.com superuser.com serverfault.com and stackoverflow.com.
Features:
- Themeable UI
- DOM HTML parser
- Save Questions Offline
- Search AND, OR, Phrase, body and title
- FREE!
At present the version available in store is v0.1, which does not include most of the features seen in the screenshots, an update has been submitted and will be available soon. In the meantime you can still download Stack Overflow v0.1 in the app store
Specification
- A user will be able to view news content whilst the application is on-line and off-line
- A user will be able to share a news article by e-mail.
- A user will launch the app and be able to read the latest news from The Business Desk
- A user will view a headlines list to choose a full article to view.
- A user can login
Solution
I worked with the team at engineroomapps.com <http://engineroomapps.com> on this project using using agile development techniques and collaboration tools such as basecamp, git and targetprocess. The app front end interface and logic were development by me using objective c and the SDK 3.0 API. Initially the news data was proposed to be supplied using Ruby & activeresource, however the library was eventually discarded due to stability issues. Instead the feed was provided by the web service team in xml format and parsed on the app side using touchxml. For offline viewing data is saved persistently using core data, reachability code is used to display when reading news online or offline.
The business desk app is available FREE in the app store
#include
#include
#include
typedef struct { int ID; char * title; char * link; char * date; int upVotes; int downVotes; void * next; } Article; Article * root; char * getVal(char * query, char * key) { char * str = malloc(1); int match = 0; int pMatch = 0; char * cur = query; while (*cur != '\0') { if (pMatch >= 1 && (*cur == '&' || *cur == '\n' || *cur == ' ')) { break; } else if (pMatch >= 1) { str = realloc(str, pMatch+1); *(str+pMatch-1) = *cur; ++pMatch; } else if (match == strlen(key)) { pMatch = 1; } else if (*cur == key[match]) { ++match; } else { match = 0; } cur++; } if (pMatch >= 1) { str[pMatch-1] = '\0'; return str; } free(str); return NULL; } char from_hex(char ch) { return isdigit(ch) ? ch - '0' : tolower(ch) - 'a' + 10; } char to_hex(char code) { static char hex[] = "0123456789abcdef"; return hex[code & 15]; } char *url_decode(char *str) { char *pstr = str, *buf = malloc(strlen(str) + 1), *pbuf = buf; while (*pstr) { if (*pstr == '%') { if (pstr[1] && pstr[2]) { *pbuf++ = from_hex(pstr[1]) << 4 | from_hex(pstr[2]); pstr += 2; } } else if (*pstr == '+') { *pbuf++ = ' '; } else { *pbuf++ = *pstr; } pstr++; } *pbuf = '\0'; return buf; } void send_headers(FILE *f, int status, char *title, char *extra, char *mime, int length) { char timebuf[128]; fprintf(f, "%s %d %s\r\n", "HTTP/1.0", status, title); fprintf(f, "Server: %s\r\n", "redditserver/1.0"); if (extra) fprintf(f, "%s\r\n", extra); if (mime) fprintf(f, "Content-Type: %s\r\n", mime); if (length >= 0) fprintf(f, "Content-Length: %d\r\n", length); fprintf(f, "Connection: close\r\n"); fprintf(f, "\r\n"); } void reddit(FILE *f) { fprintf(f, "Reddit Clone\r\n"); fprintf(f, "Submit
\r\n"); if (root) { fprintf(f, "
Articles
"); Article * cur = root; while (cur != NULL) { fprintf(f, "
%d&uarr%s &darr%s -- %s
", cur->upVotes - cur->downVotes, cur->ID, cur->link, cur->title, cur->ID, cur->link, cur->date); cur = cur->next; } }fprintf(f, "\r\n"); } int fsize(FILE * f) { fseek(f, 0, SEEK_END); int size = ftell(f); fseek(f, 0, SEEK_SET); return size; } int main(int argc, char *argv[]) { int sock; int port = 1337; struct sockaddr_in sin; sock = socket(AF_INET, SOCK_STREAM, 0); sin.sin_family = AF_INET; sin.sin_addr.s_addr = INADDR_ANY; sin.sin_port = htons(port); while (bind(sock, (struct sockaddr *) &sin, sizeof(sin)) < 0) { printf("Failed bind\n"); sleep(2); } listen(sock, 5); printf("HTTP server listening on port %d\n", port); root = NULL; while (1) { int s; int size; int hsize; char * buff; FILE * t; FILE * ht; FILE * f; s = accept(sock, NULL, NULL); if (s < 0) break; char * header = calloc(255, 1); size = read(s, header, 255); char * titleEnc = getVal(header, "title"); char * linkEnc = getVal(header, "link"); if (titleEnc && linkEnc) { char * title = url_decode(titleEnc); char * link = url_decode(linkEnc); free(linkEnc); free(titleEnc); printf("Title: %s -- Link: %s\n", title, link); char * s = malloc(30); size_t i; struct tm tim; time_t now; now = time(NULL); tim = *(localtime(&now)); i = strftime(s,30,"%b %d, %Y; %H:%M:%S\n",&tim); Article * new = malloc(sizeof(Article)); new->title = title; new->link = link; new->next = NULL; new->date = s; new->upVotes = 0; new->downVotes = 0; if (!root) { root = new; new->ID = 0; } else { int dupe = 0; Article * cur = root; Article * prev = NULL; while (cur != NULL) { if (strcmp(cur->title, new->title) == 0 || strcmp(cur->link, new->link) == 0) { dupe = 1; } prev = cur; cur = cur->next; } if (dupe == 0) { new->ID = prev->ID + 1; prev->next = new; } else free(new); } } char * upID = getVal(header, "up"); if (upID) { int ID = atoi(upID); Article * cur = root; while (cur != NULL) { if (cur->ID == ID) { cur->upVotes++; break; } cur = cur->next; } } char * dID = getVal(header, "down"); if (dID) { int ID = atoi(dID); Article * cur = root; while (cur != NULL) { if (cur->ID == ID) { cur->downVotes++; break; } cur = cur->next; } } t = tmpfile(); ht = tmpfile(); reddit(t); size = fsize(t); send_headers(ht, 200, "Ok", NULL, "text/html", size); hsize = fsize(ht); buff = malloc(hsize); fread(buff, 1, hsize, ht); write(s,buff,hsize); free(buff); buff = malloc(size); fread(buff, 1, size, t); write(s,buff,size); free(buff); fclose(t); } }

$ gcc reddit.c -o reddit
$ ./reddit
HTTP server listening on port 1337 --- http://localhost:1337/
reddit.c
Working with core data in a multi threaded environment you have to extremely careful
For example if were creating a news application, we want to be able to import news in the background and display updates to the user with no interruption of the GUI. The task can be divided into several stages:
- Download the xml feed with the latest news
- Parse the feed
- Insert the articles into core data
- Notify the UI and refresh the display.
We already have our core data API defined. A simple method to retrieve all the articles and a method to add a new article.
DataSource.h
@interface DataSource : NSObject { }
-(NSArray*)getNewsFor:(Region*)region;
-(void)addArticle:(Article*)article forRegion:(Region*)region;
@end
}
Now suppose we want to the web service to import the latest articles. We might define our implementation something like this:
WebService.h
@interface WebService : NSObject {
id delegate;
LocalDataSource * source;
}
@property(nonatomic, retain) id delegate;
@property(nonatomic, retain) LocalDataSource * source;
-(void)importNewsForRegion:(Region*)region;
@end
Our implementation of importNewsForRegion would look something like this:
WebService.m
-(void)importNewsForRegion:(Region*)region
{
//Perform in background if called from main
if ([NSThread currentThread] == [NSThread mainThread])
{
[self performSelectorInBackground:@selector(importNewsForRegion:) withObject:region];
return;
}
//Setup pool
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
NSArray * articles = [XML parseArticleFeed:region.feedURL]; //Not safe 1.
for (Article * article in articles)
{
[source addArticle:article forRegion:region]; //Not safe 2.
}
[delegate finishedImport]; //Not safe 3.
[pool drain];
}
However this because importNewsForRegion is running in a separate thread this is unsafe and will likely cause core data to crash or corrupt our database.
- The region passed to to importNews has likely come from a core data context which was constructed on the main thread. Even though were not even altering the region directly with core data it’s not safe to even read the feedURL, because every operation can trigger faulting.
- When we instruct the source to add an article it will use the main threads context, this is fine when we are in the main thread, but in a background thread we need to specify our own context.
- Not related to core data, but when we notify the delegate it will likely perform UI updates which are not safe to perform in a background thread. we need to ensure the delegate only received notifications on the main thread.
To make our program thread safe we first need to make some small alterations to our API.
DataSource.h
@interface DataSource : NSObject { }
-(NSArray*)getNewsFor:(Region*)region;
-(NSArray*)getNewsFor:(Region*)region context:(NSManagedObjectContext*)context;
-(void)addArticle:(Article*)article forRegion:(Region*)region;
-(void)addArticle:(Article*)article forRegion:(Region*)region context:(NSManagedObjectContext*)context;
@end
}
In our data source we specify two more methods which allow us to provide a context for the method to use. All our queries should be updated to only use the provided context. The original methods that don’t specify a context should call the newly implemented method with the default context, e.g.
-(void)addArticle:(Article*)article forRegion:(Region*)region
{
[self addArticle:article forRegion:region context:mainThreadContext];
}
Only when we are using core data in a background thread do we need to use the newly added methods. So we can now update our WebService.m:
-(void)importNewsForRegion:(Region*)unsafeRegion
{
//Perform in background if called from main
if ([NSThread currentThread] == [NSThread mainThread])
{
[self performSelectorInBackground:@selector(importNewsForRegion:) withObject: unsafeRegion];
return;
}
//Setup pool
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
//Create a new context just for this thread
NSManagedObjectContext * managedObjectContext = [[NSManagedObjectContext alloc] init];
[managedObjectContext setPersistentStoreCoordinator: [source persistentStoreCoordinator]];
//Create a new region which is safe to use in this thread
Region * region = (Region*)[managedObjectContext existingObjectWithID:[unsafeRegion objectID] error:nil];
NSArray * articles = [XML parseArticleFeed:region.feedURL];
for (Article * article in articles)
{
//Ensure we provide the context for the source to use
[source addArticle:article forRegion:region context:managedObjectContext];
}
//Save changes and close the new context
[managedObjectContext save:&error];
[managedObjectContext release];
[delegate performSelectorOnMainThread:@selector(finishedImport) withObject:nil waitUntilDone:NO];
[pool drain];
}
As you can see we crate a new context using out shared persistentStoreCoordinator, we ensure that anytime we touch core data from this thread we are using this context. The also means that we have to create a new region which belongs to this context, and we do this with the existingObjectWithID method and using the [unsafeRegion objectID]. Finally we ensure that the delegate receives the notification on the main thread.
for more information about threading in core data see:
http://developer.apple.com/Mac/library/documentation/Cocoa/Conceptual/CoreData/Articles/cdMultiThreading.html
Cocos2d is a popular 2d framework for developing iphone games, including physics and particle support. Here’s a quick introduction to getting cocos2d for the iphone up and running for your next project.
Download the latest source from:
http://code.google.com/p/cocos2d-iphone/downloads/list
I recommend you stick with the stable releases, unless there is a specific feature your require from a new version.
Unzip the folder and the template installation script:
$ ./install_template.sh
This will make a series of cocos-2d templates available from XCode. Launch/restart xcode and choose File->New Project->cocos2d Application build and run the new application in the simulator and it should look something like this:

Were going to build on this template to add a main menu
First download the test image and add it to the new xcode project (Make sure it is copied into the application directory)

Create a new class name MenuScene and add the following code to the header file:
#import
#import "cocos2d.h"
@interface MenuScene : Scene
{
}
@end
@interface MenuLayer : Layer
{
}
-(void)startGame: (id)sender;
-(void)help: (id)sender;
@end
The add the following to the .m file:
#import "MenuScene"
@implementation MenuScene
- (id) init
{
self = [super init];
if (self != nil) {
Sprite * bg = [Sprite spriteWithFile:@"MainMenuBackground.png"];
[bg setPosition:ccp(160, 240)];
[self addChild:bg z:0];
[self addChild:[MenuLayer node] z:1];
}
return self;
}
@end
@implementation MenuLayer
- (id) init
{
self = [super init];
if (self != nil)
{
[MenuItemFont setFontSize:24];
[MenuItemFont setFontName:@"Helvetica"];
MenuItem *start = [MenuItemFont itemFromString:@"Start Game"
target:self
selector:@selector(startGame:)];
MenuItem *help = [MenuItemFont itemFromString:@"Help"
target:self
selector:@selector(help:)];
Menu *menu = [Menu menuWithItems:start, help, nil];
menu.color = ccc3(0, 0, 0);
[menu alignItemsVertically];
[self addChild:menu];
}
return self;
}
-(void)startGame: (id)sender
{
NSLog(@"start game");
}
-(void)help: (id)sender {
NSLog(@"help");
}
@end
Finally modify the app delegate (CocosTestAppDelegate.m) to include MenuScene.h
#import "MainMenuScene.h"
and replace the line:
[[Director sharedDirector] runWithScene: [HelloWorld scene]];
with:
[[Director sharedDirector] runWithScene: [MenuScene node]];
also remove the line:
[[Director sharedDirector] setDeviceOrientation:CCDeviceOrientationLandscapeLeft];
Compile and run the project and you should end up with a menu and two clickable buttons:
