XML Tutorial for iOS: How To Read and Write XML Documents with GDataXML

A XML tutorial for iOS on how to easily read and write XML documents with Google’s open source XML processing library: GDataXML. By Ray Wenderlich.

Leave a rating/review
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

Converting the XML to our Model Objects

Ok now that we see that it’s working so far, let’s continue the code to walk through the DOM tree and create instances of our model objects as we go.

The replace the code in the loadParty method starting with replacing the NSLog statement with the following:

Party *party = [[[Party alloc] init] autorelease];
NSArray *partyMembers = [doc.rootElement elementsForName:@"Player"];
for (GDataXMLElement *partyMember in partyMembers) {
            
    // Let's fill these in!
    NSString *name;
    int level;
    RPGClass rpgClass;

    // Name
    NSArray *names = [partyMember elementsForName:@"Name"];
    if (names.count > 0) {
        GDataXMLElement *firstName = (GDataXMLElement *) [names objectAtIndex:0];
        name = firstName.stringValue;
    } else continue;
            
    // Level
    NSArray *levels = [partyMember elementsForName:@"Level"];
    if (levels.count > 0) {
        GDataXMLElement *firstLevel = (GDataXMLElement *) [levels objectAtIndex:0];
        level = firstLevel.stringValue.intValue;
    } else continue;
    
    // Class
    NSArray *classes = [partyMember elementsForName:@"Class"];
    if (classes.count > 0) {
        GDataXMLElement *firstClass = (GDataXMLElement *) [classes objectAtIndex:0];
        if ([firstClass.stringValue caseInsensitiveCompare:@"Fighter"] 
            == NSOrderedSame) {
            rpgClass = RPGClassFighter;
        } else if ([firstClass.stringValue caseInsensitiveCompare:@"Rogue"] 
            == NSOrderedSame) {
            rpgClass = RPGClassRogue;
        } else if ([firstClass.stringValue caseInsensitiveCompare:@"Wizard"] 
            == NSOrderedSame) {
            rpgClass = RPGClassWizard;
        } else {
            continue;
        }            
    } else continue;
    
    Player *player = [[[Player alloc] initWithName:name level:level 
            rpgClass:rpgClass] autorelease];
    [party.players addObject:player];

}

[doc release];
[xmlData release];
return party;

This is the real meat of our work. We use the elementsForName method on our root element to get all of the elements named “Player” underneath the root “Party” element.

Then, for each “Player” element we look to see what “Name” elements are underneath that. Our code only deals with one name, so we just take the first if there is more than one.

We do similar processing for “Level” and “Class”, but for level we convert the string into an int, and for class we convert the string into an enumeration.

If anything fails, we just skip that Player. Otherwise, we construct a Player model object with the values we read from the XML, and add it to our Party model object, and return that!

So let’s write some code to see if it works. Add the following to XMLTestAppDelegate.m in applicationDidFinishLaunching, after the call to loadParty:

if (_party != nil) {
    for (Player *player in _party.players) {
        NSLog(@"%@", player.name);
    }
}

Compile and run your project, and if all works well you should see the following in the console output:

2010-03-17 12:33:04.301 XMLTest[2531:207] Butch
2010-03-17 12:33:04.303 XMLTest[2531:207] Shadow
2010-03-17 12:33:04.304 XMLTest[2531:207] Crak

Querying with XPath

XPath is a simple syntax you can use to identify portions of an XML document. The easiest way to get a handle on it is by seeing a few examples.

For example, the following XPath expression would identify all of the Player elements in our document:

//Party/Player

And the following would just identify the first Player element in our document:

//Party/Player[1]

And finally, the following would identify the player with the name of Shadow:

//Party/Player[Name="Shadow"]

Let’s see how we could use XPath by slightly modifying our loadParty method. Replace the line that loads the party members as the following:

//NSArray *partyMembers = [doc.rootElement elementsForName:@"Player"];
NSArray *partyMembers = [doc nodesForXPath:@"//Party/Player" error:nil];

If you run the code, you’ll see the exact same results. So there isn’t an advantage of using XPath in this case, since we are interested in reading the entire XML document and constructing a model in memory.

However, you can imagine that this could be pretty useful if we had a big complicated XML document and we wanted to quickly dig down to find a particular element, without having to look through the children of node A, then the children of node B, and so on until we find it.

If you are interested in learning more about XPath, check out a nice XML tutorial from W2Schools. Also, I’ve found this online XPath expression testbed quite handy when trying to construct XPath expressions.

Saving Back to XML

So far we’ve only done half of the picture: reading data from an XML document. What if we want to add a new player to our party and then save the new document back to disk?

Well, the first thing we need to do is determine where we are going to save the XML document. So far we’ve been loading the XML document from our application’s bundle. We can’t save to the bundle, however, because it is read-only. But we can save to the application’s document directory, so let’s do that.

Modify your dataFilePath method in PartyParser.m to read as follows:

+ (NSString *)dataFilePath:(BOOL)forSave {
    
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, 
        NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    NSString *documentsPath = [documentsDirectory
        stringByAppendingPathComponent:@"Party.xml"];
    if (forSave || 
        [[NSFileManager defaultManager] fileExistsAtPath:documentsPath]) {
        return documentsPath;
    } else {
        return [[NSBundle mainBundle] pathForResource:@"Party" ofType:@"xml"];
    }
    
}

Note that when we’re loading the XML, we want to load from the file in the documents directory if it exists, but otherwise fall back to reading the XML file that ships with the app, like we’ve been doing so far.

Now, let’s write a method to construct our XML document from our data model and save it out to disk. Add a new methdo to PartyParser.m as follows:

+ (void)saveParty:(Party *)party {
 
    GDataXMLElement * partyElement = [GDataXMLNode elementWithName:@"Party"];
    
    for(Player *player in party.players) {
     
        GDataXMLElement * playerElement = 
            [GDataXMLNode elementWithName:@"Player"];
        GDataXMLElement * nameElement = 
            [GDataXMLNode elementWithName:@"Name" stringValue:player.name];
        GDataXMLElement * levelElement = 
            [GDataXMLNode elementWithName:@"Level" stringValue:
                [NSString stringWithFormat:@"%d", player.level]];
        NSString *classString;
        if (player.rpgClass == RPGClassFighter) {
            classString = @"Fighter";
        } else if (player.rpgClass == RPGClassRogue) {
            classString = @"Rogue";
        } else if (player.rpgClass == RPGClassWizard) {
            classString = @"Wizard";
        }        
        GDataXMLElement * classElement = 
            [GDataXMLNode elementWithName:@"Class" stringValue:classString];
        
        [playerElement addChild:nameElement];
        [playerElement addChild:levelElement];
        [playerElement addChild:classElement];
        [partyElement addChild:playerElement];
    }
    
    GDataXMLDocument *document = [[[GDataXMLDocument alloc] 
            initWithRootElement:partyElement] autorelease];
    NSData *xmlData = document.XMLData;
    
    NSString *filePath = [self dataFilePath:TRUE];
    NSLog(@"Saving xml data to %@...", filePath);
    [xmlData writeToFile:filePath atomically:YES];
        
}

As you can see, GDataXML makes it quite easy and straightforward to construct our XML document. You simply create elements with elementWithName: or elementWithName:stringValue, connect them to each other with addChild, and then create a GDataXMLDocument specifying the root element. In the end, you get an NSData that you can easily save to disk.

Next declare your new method in PartyParser.h:

+ (void)saveParty:(Party *)party;

Then inside the party != nil check in applicationDidFinishLaunching, go ahead and add a new party member to our list:

[_party.players addObject:[[[Player alloc] initWithName:@"Waldo" level:1 
    rpgClass:RPGClassRogue] autorelease]];

And finally let’s save out the updated party list in applicationWillTerminate:

- (void)applicationWillTerminate:(UIApplication *)application {
    [PartyParser saveParty:_party];   
}

Compile and run your project, and after the app loads go ahead and exit. The app should print out to the console where it saves your XML. It will look something like this:

2010-03-17 13:34:14.447 XMLTest[3118:207] Saving xml data to 
/Users/rwenderlich/Library/Application Support/iPhone Simulator/User/
Applications/BF246A72-7E20-47CF-93FF-AA2CEF50A6B0/Documents/Party.xml..

Go ahead and find that folder with Finder and open up the XML, and if all goes well you should see your new party member in the XML:

Screenshot of our Modified XML

Then run the app again, open up your console log, and see if you can find where Waldo is! :]