iOS中如何利用GDataXML对XML文档进行读写

来源:互联网 发布:手机淘宝红包怎么使用 编辑:程序博客网 时间:2024/05/16 00:59


Let's read and write this!Let's read and write this!本文译自:raywenderlich

在上一篇文章:在iOS工程中如何选择最佳的XML解析器中,Saliom建议我写一篇文章介绍一下如何使用XML解析器来读写XML文档,并根据XML文档创建自己的对象,以及执行XPath查询。

本文我就告诉你该如何去做!我将创建一个工程,来读取XML示例文档中的内容(包含RPG玩家列表),并基于该XML文档构建我们自己的对象。接着我将在成员列表中添加一个新的玩家,然后再将其保存回磁盘中。

这里我使用的是GDataXML — Google解析XML的一个库。之所选择GDataXML是因为它针对DOM解析的执行效率不错,并且支持XML文档的读写,也很容易集成到工程中。当然,如果你使用别的DOM解析器,大多数操作都是一样的,只不过API的调用有轻微的不同。

特别感谢Saliom建议我写这篇文章!

我的XML文档

下图是我在本文中使用到的XML文档示例:

Screenshot of our XML documentScreenshot of our XML document

之前我提到过,上面的这个列表代表了一个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;@endAnd 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:"Butch1FighterShadow2RogueCrak3Wizard"}

将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] Butch2010-03-17 12:33:04.303 XMLTest[2531:207] Shadow2010-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文件中已经新增了一个玩家:

Screenshot of our Modified XMLScreenshot of our Modified XML

然后在运行一次程序,并打开控制台窗口,你将看到Waldo玩家!:]

总结

这里是本文的示例代码。注意,该程序不会在GUI中显示任何内容(这并不重要)——只是在控制台显示一些内容。

另外 — 在工程中使用XML之前,应该花点时间考虑一下,为什么要在工程中使用XML,这是最佳选择吗。在这里的示例中,如果我们只是加载和保存一些数据,使用XML可能是大材小用了,使用别的一些序列化格式可能会更好:例如plist,NSCoding或者Core Data。

不过,在多个程序中都使用相同的数据,那么XML是个不错的选择。如果我们用Mac程序生成了一个列表数据,而我们希望iPhone程序能够对该列表进行读写,那么XML将非常有用。这也是为什么XML是数据交换的标准格式(非常容易协同利用)。

至此,在写程序的时候,你计划使用XML了吗?


原创粉丝点击