在上一篇文章:在iOS工程中如何选择最佳的XML解析器中,Saliom建议我写一篇文章介绍一下如何使用XML解析器来读写XML文档,并根据XML文档创建自己的对象,以及执行XPath查询。
本文我就告诉你该如何去做!我将创建一个工程,来读取XML示例文档中的内容(包含RPG玩家列表),并基于该XML文档构建我们自己的对象。接着我将在成员列表中添加一个新的玩家,然后再将其保存回磁盘中。
这里我使用的是GDataXML — Google解析XML的一个库。之所选择GDataXML是因为它针对DOM解析的执行效率不错,并且支持XML文档的读写,也很容易集成到工程中。当然,如果你使用别的DOM解析器,大多数操作都是一样的,只不过API的调用有轻微的不同。
特别感谢Saliom建议我写这篇文章!
我的XML文档
下图是我在本文中使用到的XML文档示例:
之前我提到过,上面的这个列表代表了一个RPG游戏中玩家。这里我尽量让XML数据简洁,当然,我还添加了一些有趣的数据。
OK,下面我们就开始在工程中对该XML文档内容进行读写吧!
集成GDataXML
按照下面的步骤,就可以把GDataXML集成到新的工程中:
- 选择Project\New Project, 然后选择View-based Application, 将工程命名为XMLTest。
- 下载gdata-objective-c client library。
- 解压下载的文件,定位到Source\XMLSupport, 然后将GDataXMLNode.h 和 GDataXMLNode.m两个文件拖入工程中。
- 在XCode中, 点击Project\Build Settings 并确保 “All”选中。
- 找到Search Paths\Header Search Paths并添加/usr/include/libxml2。
- 然后,找到Linking\Other Linker Flags并添加-lxml2到列表中。
- 在 XMLTestAppDelegate.h的顶部添加如下代码,可以测试上面的步骤是否正确:
#import "GDataXMLNode.h"
如果程序能够编译并运行,说明GDataXML集成成功了!
创建模型类
下一步,我们创建一组模型类来代表XML文档中的玩家。
首先创建一个Player类。点击File\New File, 选择Cocoa Touch\Objective-C class, 点击Next之后,选择Subclass of NSObject, 然后在点击Next,将文件命名为Player。
然后用下面的代码替换Player.h文件:
#import <Foundation/Foundation.h> typedef enum { RPGClassFighter, RPGClassRogue, RPGClassWizard } RPGClass; @interface Player : NSObject { NSString *_name; int _level; RPGClass _rpgClass; } @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
接着用下面的代码替换Player.m文件:
#import "Player.h" @implementation Player @synthesize name = _name; @synthesize level = _level; @synthesize rpgClass = _rpgClass; - (id)initWithName:(NSString *)name level:(int)level rpgClass:(RPGClass)rpgClass {Class:(RPGClass)rpgClass { if ((self = [super init])) { self.name = name; self.level = level; self.rpgClass = rpgClass; } return self; } - (void) dealloc { self.name = nil; [super dealloc]; } @end
上面所涉及到的都是Objective-C, 现在没有真正的涉及到XML。为了完整性,这里我将其全部列出来。
按照上面创建一个NSObject子类的步骤,在创建一个名为Party的类。然后用下面的代码替换Party.h文件:
#import <Foundation/Foundation.h> @interface Party : NSObject { NSMutableArray *_players; } @property (nonatomic, retain) NSMutableArray *players; @end And Party.m with the following: #import "Party.h" @implementation Party @synthesize players = _players; - (id)init { if ((self = [super init])) { self.players = [[[NSMutableArray alloc] init] autorelease]; } return self; } - (void) dealloc { self.players = nil; [super dealloc]; } @end
下面我们开始解析XML文件,并创建基于XML文件内容的实例对象。
使用GDataXML解析XML
下面我们使用GDataXML来读取XML文档内容,并在内存中创建一棵DOM树。
首先,下载Party.xml文件,并将其添加到工程中。
接着,创建一个新的类,里面包含了所有相关解析的代码。在工程中添加一个名为PartyParser的新类,该类继承自NSObject
用下面的代码替换PartyParser.h文件:
#import <Foundation/Foundation.h> @class Party; @interface PartyParser : NSObject { } + (Party *)loadParty; @end
然后用下面的代码替换PartyParser.m文件:
#import "PartyParser.h" #import "Party.h" #import "GDataXMLNode.h" #import "Player.h" @implementation PartyParser + (NSString *)dataFilePath:(BOOL)forSave { return [[NSBundle mainBundle] pathForResource:@"Party" ofType:@"xml"]; } + (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; } NSLog(@"%@", doc.rootElement); [doc release]; [xmlData release]; return nil; }
@end
上面的代码中,我首先创建了一个静态方法dataFilePath,该方法返回添加到工程中(成为程序bundle的一部分)Party.xml文件的路径。注意,该方法有一个boolean参数值,表示我们对该路径中的文件进行读或写—现在还不使用,稍后会用到。
接着,我创建了另外一个静态方法loadParty。该方法会使用NSMutableData的initWithContentsOfFile方法将粗盘中文件的内容以字节的形式加载到内存中,然后将数据传递给GDataXML解析器(通过GDataXMLDocument的initWithData方法)。
如果GDataXML在解析文件过程中遇到了错误(比如缺少结束标记),那么会返回nil和一个NSError对象,该对象有相关错误信息。本文中,如果发生了错误,我只返回一个nil。
在上面的代码中,如果解析成功,我将把XML文档的root元素打印出来。
下面,我们就来使用上面这个方法吧。将XMLTestAppDelegate.h文件按照如下修改:
#import "XMLTestAppDelegate.h" #import "XMLTestViewController.h" #import "PartyParser.h" #import "Party.h" #import "Player.h" @implementation XMLTestAppDelegate @synthesize window; @synthesize viewController; @synthesize party = _party; - (void)applicationDidFinishLaunching:(UIApplication *)application { self.party = [PartyParser loadParty]; // Override point for customization after app launch [window addSubview:viewController.view]; [window makeKeyAndVisible]; } - (void)dealloc { self.party = nil; [viewController release]; [window release]; [super dealloc]; } @end
在上面的代码中,我只是import了几个文件,并在applicationDidFinishLaunching方法中调用之前定义的静态方法以加载party,另外在dealloc方法中做了一些清除任务。
现在编译并运行程序,通过Run\Console,并滚到Console窗口底部,将看到类似如下的一些信息:
2010-03-17 11:31:21.467 XMLTest[1568:207] GDataXMLElement 0x3b02530: {type:1 name:Party xml:"Butch 1Fighter Shadow2Rogue Crak3 Wizard"}
将XML转换为模型对象
从上面的介绍中,我们可以看到解析器已经开始起作用了,下面我们继续解析整棵DOM树,并创建出我们的模型对象。
将loadParty方法中从NSLog语句开始处,用下面的代码进行替换:
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;
在上面的代码中,我在root节点上使用GDataXMLElement的elementsForName方法来获取“Party”节点下所有名称为“Player”的节点。
然后,在每个“Player”节点下,我都会去查找“Name节点素。上面的代码中只处理一个名字,所以,如果有多于一个以上的名字,那么我只会去第一个。
同样,针对“Level”和“Class”节点,也做相同的处理,只不过我会把level从字符串转换为一个整型,class转换为枚举。
操作过程中,如果失败了,那么只是忽略掉Player。如果,一切正确的话,那么我将根据从XML中读取出来的数据创建一个Player对象,并将其添加到Party中,最后将Party返回!
下面,我们就来看看上面的方法是否正确。将下面的代码添加到XMLTestAppDelegate.m文件中applicationDidFinishLaunching方法里面的loadParty调用后面:
if (_party != nil) { for (Player *player in _party.players) { NSLog(@"%@", player.name); } }
编译并运行程序,如果一切正常的话,可以在控制台看到如下内容:
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
使用XPath进行查询
XPath是一种简单的语法,可以用来查询XML文档中的内容。掌握XPath的最佳方法就是通过一些示例。
例如,下面的XPath表达式是用来查询XML文档中所有的Player节点:
//Party/Player
而下面的表达式则是查询出XML文档中第一个Player节点:
//Party/Player[1]
最后,下面这个表达式则是查询出名字为Shadow的Player节点:
//Party/Player[Name="Shadow"]
下面我们稍微的修改一下loadParty方法,来看看如何使用XPath。替换一下加载party的代码,如下所示:
//NSArray *partyMembers = [doc.rootElement elementsForName:@"Player"]; NSArray *partyMembers = [doc nodesForXPath:@"//Party/Player" error:nil];
如果现在运行程序,会看到跟之前一样的结果。其实这并不是XPath的一个优点,因为在这里我们是要读取出XML文档中所有内容,并在内存中构造一个数据模型。
不过,你可以想象一下,如果这里有一个大的,很复杂的XML文档,我们希望迅速的找到指定的节点,但是并不是通过查找A的子节点,然后是B的子节点,直到找到指定节点,那么此时,XPath将非常有用。
如果你对XPath感兴趣的话,可以从W2Schools的教程里学到更多相关内容。同样,这里我还发现一个在线测试XPath表达式的一个网站,非常方便。
保存回XML
到现在为止,我们只完成了一半的任务:从XML文档中读取出数据。如果我们想要添加新的player,并将新的文档保存回磁盘中,该如何操作呢?
首先,我们需要做的事情是确定一下将XML文档保存到哪里。之前,我们已经从程序的bundle中加载XML文档了,不过,那是只读的。不过,我们可以将其保存到程序的document目录中。下面就来试试吧。
修改一下PartyParser.m文件中的dataFilePath方法,如下代码:
+ (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"]; } }
注意,在加载XML时,如果document目录中存在XML文件,那么就从document中加载,否则就从程序的bundle中加载。
下面,我来写一个方法:根据我们的数据模型来构造一个XML文档,并将其保存到磁盘中。如下代码,将新方法添加到PartyParser.m文件中:
+ (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]; }
如上代码所示,使用GDataXML来构造XML文档非常的简单和直接。通过elementWithName:或elementWithName:stringValue方法就可以创建节点,并通过addChild就能把这些节点关联起来,然后根据指定的根节点就可以创建一个GDataXMLDocument。最后,获得相关的NSData,并将其保存到磁盘中。
下面,在PartyParser.h文件中声明这个新方法:
+ (void)saveParty:(Party *)party;
然后在applicationDidFinishLaunching中做一下判断:party != nil,并在列表中添加一个新的玩家:
[_party.players addObject:[[[Player alloc] initWithName:@"Waldo" level:1 rpgClass:RPGClassRogue] autorelease]];
最后applicationWillTerminate方法中将更新的玩家列表保存起来:
- (void)applicationWillTerminate:(UIApplication *)application { [PartyParser saveParty:_party]; }
编译并运行程序,程序加载完毕之后,退出程序。程序将会打印出XML文件保存的位置。如下:
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..
接着通过Finder找到对应的文件夹,并打开XML文件,如果一切正常的话,在XML文件中已经新增了一个玩家:
然后在运行一次程序,并打开控制台窗口,你将看到Waldo玩家!:]
总结
这里是本文的示例代码。注意,该程序不会在GUI中显示任何内容(这并不重要)——只是在控制台显示一些内容。
另外 — 在工程中使用XML之前,应该花点时间考虑一下,为什么要在工程中使用XML,这是最佳选择吗。在这里的示例中,如果我们只是加载和保存一些数据,使用XML可能是大材小用了,使用别的一些序列化格式可能会更好:例如plist,NSCoding或者Core Data。
不过,在多个程序中都使用相同的数据,那么XML是个不错的选择。如果我们用Mac程序生成了一个列表数据,而我们希望iPhone程序能够对该列表进行读写,那么XML将非常有用。这也是为什么XML是数据交换的标准格式(非常容易协同利用)。
至此,在写程序的时候,你计划使用XML了吗?