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


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;

@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文件中已经新增了一个玩家:

Screenshot of our Modified XMLScreenshot of our Modified XML

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

总结

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

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

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

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


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值
>