@Xiaojun-Jin
2014-09-09T08:11:29.000000Z
字数 6562
阅读 2430
iOS
XML
Note from author: This is a learning note of XML Tutorial for iOS: How To Read and Write XML Documents with GDataXML written by Ray Wenderlich. This post focuses mainly on how to use an XML library to read and write XML documents, create your own objects based on the documents, and perform XPath queries.
We're going to work with a simple XML document in this XML tutorial that looks like the following:
<Party>
<Player>
<Name>Butch</Name>
<Level>1</Level>
<Class>Fighter</Class>
</Player>
<Player>
<Name>Shadow</Name>
<Level>2</Level>
<Class>Rogue</Class>
</Player>
<Player>
<Name>Crak</Name>
<Level>3</Level>
<Class>Wizard</Class>
</Player>
</Party>
First of all, we have to integrate GDataXML into our project with the fowllowing steps:
Next, let's create a set of model classes (Player & Party) that we want to represent our party of XML characters in our game.
typedef enum {
RPGClassFighter,
RPGClassRogue,
RPGClassWizard
} RPGClass;
@interface Player : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int level;
@property (nonatomic, assign) RPGClass rpgClass;
- (id)initWithName:(NSString *)name level:(int)level rpgClass:(RPGClass)rpgClass;
@end
All of this is straight Objective-C, nothing really to do with XML at this point. However, I'm still including it for completeness sake.
@interface Party : NSObject
@property (nonatomic, retain) NSMutableArray *players;
@end
So far so good! Now let's get to parsing our XML file and creating instances of these objects based on the XML. First, we creat a new class named PartyParser to hold all of our parsing code.
Replace PartyParse.h with the following:
@class Party;
@interface PartyParser : NSObject
+ (Party *)loadParty;
+ (void)saveParty:(Party *)party;
@end
And then replace PartyParser.m with the following:
#import "PartyParser.h"
#import "Party.h"
#import "GDataXMLNode.h"
#import "Player.h"
@implementation PartyParser
+ (NSString *)dataFilePath:(BOOL)forSave
{
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
NSUserDomainMask, YES);
NSString *docDirectory = [paths objectAtIndex:0];
NSString *docPath = [documentsDirectory stringByAppendingPathComponent:@"Party.xml"];
if (forSave || [[NSFileManager defaultManager] fileExistsAtPath:documentsPath])
{
return documentsPath;
}
else
{
return [[NSBundle mainBundle] pathForResource:@"Party" ofType:@"xml"];
}
}
@end
Ok let's continue the code to walk through the DOM tree and convert the XML to model objects as we go.
+ (Party *)loadParty
{
NSString *filePath = [self dataFilePath:FALSE];
NSData *xmlData = [[NSMutableData alloc] initWithContentsOfFile:filePath];
NSError *error;
GDataXMLDocument *doc = [[GDataXMLDocument alloc] initWithData:xmlData
options:0 error:&error];
if (doc == nil) return nil;
Party *party = [[[Party alloc] init] autorelease];
NSArray *partyMembers = [doc.rootElement elementsForName:@"Player"];
// NSArray *partyMembers = [doc nodesForXPath:@"//Party/Player" error:nil];
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 *fClass = (GDataXMLElement *) [classes objectAtIndex:0];
if ([fClass.stringValue caseInsensitiveCompare:@"Fighter"] == NSOrderedSame)
{
rpgClass = RPGClassFighter;
}
else if ([fClass.stringValue caseInsensitiveCompare:@"Rogue"] == NSOrderedSame)
{
rpgClass = RPGClassRogue;
}
else if ([fClass.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. For "Level" and "Class", we do the similar processing.
Querying with XPath:
XPath is a simple syntax you can use to identify portions of an XML document.//Party/Player
this expression would identify all of the Player elements in our document. Consequently, we can use[doc nodesForXPath:@"//Party/Player" error:nil]
to query the Player elements (see the comment line in the above code).
After that, we can write some code to see if it works.
self.party = [PartyParser loadParty];
if (_party != nil)
{
for (Player *player in _party.players)
{
NSLog(@"%@", player.name);
}
}
So far we've only done half of the picture - reading data from an XML document. Now let's write a method to construct our XML document from our data model and save it out to disk. Add a new method 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.
Finally, let's add a new party member to our list and see if it works as expected.
[_party.players addObject:[[[Player alloc] initWithName:@"Waldo"
level:1
rpgClass:RPGClassRogue] autorelease]];
[PartyParser saveParty:_party];
Update:
I recently found an open source class for converting XML file to NSDictionary in a pretty handy way. Please refer to XMLDictionary for an explanation of how it works.