気楽なソフト工房

プログラミングについていろいろな記事を書いています。



mykonos2008

Author:mykonos2008
システムエンジニアとして働いている30代の会社員です。
仕事や趣味でプログラムを書いている方の役に立つ記事を書いていきたいと思っています。
ご意見、ご感想はこちらまで
If you are an english speaker,Please visit my english blog.

上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。
iOSにはUIViewをアニメーションさせるための、便利なフレームワーク「Core Animation」があります。
でも、何となく使い方は分かってるけど、CALayerって何?っていうところがぼやけていたりしませんか?
そこで、本稿では、UIiewとCALayerの関係について書いてみたいと思います。

Core Animationについて


Core AnimationはViewをアニメーションさせるための基本的な仕組みを提供するもので、よく混同されがちなのですが、
Viewとは全く異なった役割を持ったものです。Core AnimationはViewのコンテンツをbitmapとしてキャッシュし、
それをGPUに直接処理させることで、アニメーションを実現するものです。
しかし、そのコンテンツそのものを準備する部分については、Viewに委ねられています。

UIViewとCALayerの関係について


Core Animationを扱う際、最も混乱しやすい部分がUIViewとCALayerの関係についてではないかと思います。

以下の2つのコードを見てください。
@implementation ViewController{
    IBOutlet UIImageView *_charactor;
}

・・・・・・・・・・・・・・・・・・・・・
-(IBAction)startButtonTouched:(id)sender
{
    [UIView animateWithDuration:1.0f animations:^{
         _charactor.center = CGPointMake(250,_charactor.center.y);
    }];
}
・・・・・・・・・・・・・・・・・・・・・


#import <QuartzCore/QuartzCore.h>

@implementation ViewController{
    IBOutlet UIImageView *_charactor;
}

・・・・・・・・・・・・・・・・・・・・・
-(IBAction)startButtonTouched:(id)sender
{
    [UIView animateWithDuration:1.0f animations:^{
        _charactor.layer.position = CGPointMake(250,_charactor.center.y);
    }];
}
・・・・・・・・・・・・・・・・・・・・・

2つのソースコードの実行結果は全く同じになります。画像を画面の左から右にアニメーションさせながら移動します。

実行前実行後


こうしてみると、ますます、UIViewとCALayerを混同してしまいそうですね。
しかし、UIViewをCALayerは全く異なった役割を持つ別のものなのです。

繰り返しになりますが、CALayerはUIViewのコンテンツをbitmapとしてキャッシュし、
その状態を変えることでアニメーションを実行する役割を担っています。
その一方で、イベントを処理したり、コンテンツを描画したりすることは出来ません。それはUIViewの役割になります。

iOSではUIViewのインスタンスには必ずそれに対応するCALayerのインスタンスが作成され、後ろに控えています。
そして、上記のサンプルコードのように、いくつかのCALayerのプロパティはUIViewのプロパティからも
アクセスできるようになっています。その場合、CALayerのプロパティとUIViewのプロパティが異なった
値を持っているわけではなく、常に同じ値を持っています。
(なお、CALayerのプロパティに直接アクセスするためには、QuartzCore/QuartzCore.hをインポートしておく
必要があります。)

一見すると、UIViewがアニメーションの機能を提供しているように見えますが、それは後ろに控えている
CALayerによって実現されているのです。

スポンサーサイト
XMLからオブジェクトのデシリアライズをするiOS用のライブラリ「ChimeraXML(キメラXML)」をgithubに公開しました。
iOSのXMLデシリアライズライブラリ「ChimeraXML」

本稿はデシリアライズシリーズの最終回です。

テーマはコレクション型の要素を扱うようにすることです。

今回使用するXMLは以下です。

<User>
  <Id>1</Id>
  <Name>山田太郎</Name>
  <Family>
    <User>
       <Name>山田次郎</Name>
     </User>
  </Family>
  <Friends>
     <User>
       <Id>2</Id>
     </User>
     <User>
       <Id>3</Id>
     </User>
  </Friends>
  <Email>xxxxx@xxxx.com</Email>
</User>

子の要素として、複数のUser要素を持つFriends要素を追加しました。

まず、デシリアライズ先のUserクラスにプロパティを追加して、propertyInfoForElement:element:にも処理を追加します。

[User.h]

#import <Foundation/Foundation.h>
#import "XmlEntity.h"

@interface User : XmlEntity

@property(strong,nonatomic) NSString *userId;
@property(strong,nonatomic) NSString *name;
@property(strong,nonatomic) NSArray *friends;
@property(strong,nonatomic) NSString *email;

@end

[User.m]
#import "User.h"

@implementation User

+(PropertyInfo *)propertyInfoForElement:(NSString *)element
{
    static NSDictionary *_propDic;
    
    if(!_propDic){
        
        PropertyInfo *userId = [[PropertyInfo alloc] init];
        userId.name = @"userId";
        userId.type = [NSString class];
        
        PropertyInfo *name = [[PropertyInfo alloc] init];
        name.name = @"name";
        name.type = [NSString class];
        
        PropertyInfo *friends = [[PropertyInfo alloc] init];
        friends.name = @"friends";
        friends.type = [NSArray class];
        friends.subType = [User class];
        
        PropertyInfo *email = [[PropertyInfo alloc] init];
        email.name = @"email";
        email.type = [NSString class];
        
        _propDic = @{
                     @"Id": userId,
                     @"Name": name,
                     @"Friends":friends,
                     @"Email" :email
                     };
    }
    
    return _propDic[element];
}
@end

上記のコードでPropertyInfoクラスのsubTypeというプロパティにUserクラスを設定しています。
これは今回、PropertyInfoクラスに追加したプロパティで、Objective-cにはジェネリックコレクションの仕組みが無いため、
NSArray型のプロパティの要素として、何のクラスのオブジェクトが登録されるかを指定するためのものです。

「PropertyInfo.h」
#import <Foundation/Foundation.h>

@interface PropertyInfo : NSObject

@property(strong,nonatomic) NSString *name;
@property(strong,nonatomic) Class type;
@property(strong,nonatomic) Class subType;

@end

そして、最後にコレクションを扱う処理を追加したパース処理クラスです。

「ParserDelegateImpl.m」
#import "ParserDelegateImpl.h"
#import "ElementInfo.h"
#import "XmlEntity.h"
#import "PropertyInfo.h"

#import 

@implementation ParserDelegateImpl{
    
    NSMutableArray *_elementStack;
    id _rootObject; //デシリアライズ結果のオブジェクト
    int _depth; //現在処理しているXMLの階層を示す変数
}

-(id)initWithTargetClass:(Class)targetClass
{
    self = [super init];
    if(self){
        _elementStack = [NSMutableArray array];
        _rootObject = [[targetClass alloc] init];
    }
    return self;
}

-(void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI 
  qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict
{
    if(_depth > [_elementStack count]){
        _depth++;
        return;
    }
    
    _depth++;   
    
    //ルート要素の場合
    if([_elementStack count] == 0){
        //最上位階層の情報を管理するElementInfoを生成し、Stackにつめる
        ElementInfo *info = [[ElementInfo alloc] init];
        info.elementName = elementName;
        info.target = _rootObject;

        [_elementStack addObject:info];
    }
    //ルート要素以外の場合
    else{
        //親要素の情報を取り出す
        ElementInfo *parent = [_elementStack lastObject];
        
        //親要素がコレクションの場合
        if([parent.target isKindOfClass:[NSArray class]]){
            ElementInfo *info = [[ElementInfo alloc] init];
            info.elementName = elementName;
            info.target = [[parent.propInfo.subType alloc] init];
            [parent.target addObject:info.target];
            [_elementStack addObject:info];
        }
        else{
            //Userクラスから要素に対応するプロパティの情報を取得する
            PropertyInfo *propInfo =[[parent.target class] propertyInfoForElement:elementName];
            if(!propInfo){
                return;
            }
            //プロパティが存在しているかチェックする
            objc_property_t prop =  class_getProperty([parent.target class], [propInfo.name UTF8String]);
            if(prop){
               
                //要素を管理するElementInfoを生成し、Stackにつめる
                ElementInfo *info = [[ElementInfo alloc] init];
                info.elementName = elementName;
                info.propInfo = propInfo;
                
                //プロパティの型に応じて処理を分ける
                //NSStringの場合
                if(propInfo.type == [NSString class]){
                    info.target = [[NSMutableString alloc] init];
                }
                //コレクションの場合
                else if(propInfo.type == [NSArray class] || propInfo.type == [NSMutableArray class]){
                    info.target = [NSMutableArray array];
                    [parent.target setValue:info.target forKey:propInfo.name];
                }
                
                [_elementStack addObject:info];
            }
        }
    }
}

-(void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName 
    namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName
{
    if(_depth == [_elementStack count]){
        //スタックにつめられている最後のオブジェクトを取得し、その要素名が引数のelementNameと一致するか判定する
        ElementInfo *lastElement = [_elementStack lastObject];
        if([lastElement.elementName isEqualToString:elementName]){
            //targetオブジェクトがNSMutableStringの場合(テキストノードの場合)
            if([lastElement.target isKindOfClass:[NSMutableString class]]){
                //もう一つ上の階層のオブジェクトのプロパティにテキストノードの値を設定する
                ElementInfo *parentElement = [_elementStack objectAtIndex:[_elementStack count] -2];
                [parentElement.target setValue:lastElement.target forKey:lastElement.propInfo.name];
            }
        }
        
        [_elementStack removeLastObject];
        
    }
    _depth--;
}

-(void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string
{
    //stackの最後のオブジェクトがNSMutableStringの場合、テキストノードの値をそれにつめる
    ElementInfo *lastElement = [_elementStack lastObject];
    if([lastElement.target isKindOfClass:[NSMutableString class]]){
        [((NSMutableString *)lastElement.target) appendString:string];
    }
}

//解析結果のオブジェクトを返却する
-(id)resultObject
{
    return _rootObject;
}

@end


これで一般的な形のXMLを扱うことが出来るものになったと思います。
実際にこれをライブラリとして使用する場合は、もう少し必要になることがあると思います。

今回のコードでは、デシリアライズ先のクラスのプロパティの型が、
NSStringとNSArray型のみでしたが、その他の型を扱えるようにする必要があると思います。

あとは、XMLが崩れていたり、デシリアライズとして、想定していないクラスが
渡されたりした場合などの例外処理と、チェック処理あたりになると思います。


XMLからオブジェクトのデシリアライズをするiOS用のライブラリ「ChimeraXML(キメラXML)」をgithubに公開しました。
iOSのXMLデシリアライズライブラリ「ChimeraXML」

前回の記事ではシンプルなXMLをデシリアライズする記事を紹介しました。

今回は、もう少し複雑なXMLを扱えるようにしたコードを紹介します。

<User>
  <Id>1</Id>
  <Name>山田太郎</Name>
  <Family>
    <User>
       <Name>山田次郎</Name>
     </User>
  </Family>
  <Email>xxxxx@xxxx.com</Email>
</User>

前回のXMLとの違いはFamilyという要素が加わっていることです。
今回の記事のテーマは、このFamily要素を処理することではなく、無視をすることです。

実際の開発の現場でも、APIのXMLに含まれる要素の一部が不要なケースは多々あると思います。
そこで、今回はUserクラスのpropertyInfoForElement:element:には手を加えず、
(つまり、処理対象として、Familyを追加せず)、その他の要素が正しく処理されるように
コードを変更します。ちなみに前回のコードで、今回のXMLを処理すると一部の要素が正しく
取得できません。

「ParserDelegateImpl.m」
#import "ParserDelegateImpl.h"
#import "ElementInfo.h"
#import "XmlEntity.h"
#import "PropertyInfo.h"

#import <objc/runtime.h>

@implementation ParserDelegateImpl{
    
    NSMutableArray *_elementStack;
    id _rootObject; //デシリアライズ結果のオブジェクト
    int _depth; //現在処理しているXMLの階層を示す変数
}

-(id)initWithTargetClass:(Class)targetClass
{
    self = [super init];
    if(self){
        _elementStack = [NSMutableArray array];
        _rootObject = [[targetClass alloc] init];
    }
    return self;
}

-(void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI
    qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict
{
    if(_depth > [_elementStack count]){
        _depth++;
        return;
    }
    
    _depth++; 
    
    //ルート要素の場合
    if([_elementStack count] == 0){
        //最上位階層の情報を管理するElementInfoを生成し、Stackにつめる
        ElementInfo *info = [[ElementInfo alloc] init];
        info.elementName = elementName;
        info.target = _rootObject;

        [_elementStack addObject:info];
    }
    //ルート要素以外の場合
    else{
        //親要素の情報を取り出す
        ElementInfo *parent = [_elementStack lastObject];
        //Userクラスから要素に対応するプロパティの情報を取得する
        PropertyInfo *propInfo =[[parent.target class] propertyInfoForElement:elementName];
        if(!propInfo){
            return;
        }
        //プロパティが存在しているかチェックする
        objc_property_t prop =  class_getProperty([parent.target class], [propInfo.name UTF8String]);
        if(prop){
            //NSString *propAttr = [NSString stringWithUTF8String:property_getAttributes(prop)];
            //NSLog(@"propAttr = %@",propAttr);
            
            //要素を管理するElementInfoを生成し、Stackにつめる
            ElementInfo *info = [[ElementInfo alloc] init];
            info.elementName = elementName;
            info.propInfo = propInfo;
            
            //プロパティの型に応じて処理を分ける
            //NSStringの場合
            if(propInfo.type == [NSString class]){
                info.target = [[NSMutableString alloc] init];
            }
            
            [_elementStack addObject:info];
        }
    }     
}

-(void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName 
     namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName
{
    if(_depth == [_elementStack count]){
        //スタックにつめられている最後のオブジェクトを取得し、その要素名が引数のelementNameと一致するか判定する
        ElementInfo *lastElement = [_elementStack lastObject];
        if([lastElement.elementName isEqualToString:elementName]){
            //targetオブジェクトがNSMutableStringの場合(テキストノードの場合)
            if([lastElement.target isKindOfClass:[NSMutableString class]]){
                //もう一つ上の階層のオブジェクトのプロパティにテキストノードの値を設定する
                ElementInfo *parentElement = [_elementStack objectAtIndex:[_elementStack count] -2];
                [parentElement.target setValue:lastElement.target forKey:lastElement.propInfo.name];
            }
        }
        
        [_elementStack removeLastObject];
        
    }
    _depth--;
}

-(void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string
{
    //stackの最後のオブジェクトがNSMutableStringの場合、テキストノードの値をそれにつめる
    ElementInfo *lastElement = [_elementStack lastObject];
    if([lastElement.target isKindOfClass:[NSMutableString class]]){
        [((NSMutableString *)lastElement.target) appendString:string];
    }
}

//解析結果のオブジェクトを返却する
-(id)resultObject
{
    return _rootObject;
}

@end

変更点はほんの少しです。

まず、現在、処理をしている階層を管理するdepthという変数を追加しています。
そして、depthをdidStartElementの最初で1プラスし、didEndElementの終わりで1マイナスしています。

たとえば、Idタグに対応するdidStartElementがコールされたタイミングでは値は1になっています。
Userタグは開始していますが、終了はしていませんので。

そして、前回少し解説しましたが、このコードではデシリアライズをする際、NSMutableArrayをスタックとして使用し、
XMLを上から下の要素に移動する毎に、通過した要素の情報(ElementInfo)をNSMutableArrayに
追加していく処理をしています。逆に、要素の終了タグが見つかったタイミングで、スタックの最後に登録されているElementInfoを
削除しています。

このロジックですと、例えば、Idタグが発見され、didStartElementがコールされたタイミングでは、
Userタグ(ルート)に対応するElementInfo1つがスタックに登録されていることになります。
従って、depthとスタックのサイズは一致していることになります。

もう一点重要なポイントとして、処理の対象としない要素を発見した際は、スタックには登録しないロジック(下記の部分)になっています。

-(void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI
    qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict
{
・・・・・・・・・・・・・・・・・
       PropertyInfo *propInfo =[[parent.target class] propertyInfoForElement:elementName];
        if(!propInfo){
            return;
        }
・・・・・・・・・・・・・・・・・
}

つまり、不要なタグを発見した以降は、depthがスタックのサイズより大きくなってしまいます。
これを利用して、didStartElement、didEndElementで処理を開始する前に
処理対象かどうかの判定を行っています。
次回でデシリアライズシリーズの最終回にしようと思いますが、 コレクションタイプの要素を扱えるようにしてみたいと思います。


前回の続きで、今回はいよいよ、デシリアライズする部分の処理を紹介します。

デシリアライズする対象のXMLは以下です。

<User>
  <Id>1</Id>
  <Name>山田太郎</Name>
  <Email>xxxxx@xxxx.com</Email>
</User>

今回は解説するより、ソースをみていただく方が早いかと思います。ですので、ここから一気にソースを紹介します。

まずはデシリアライズを起動する部分の処理。
XMLの解析にはNSXMLParserを使用します。

    NSData *xmlData = [_xml dataUsingEncoding:NSUTF8StringEncoding];
    ParserDelegateImpl *delegateImpl = [[ParserDelegateImpl alloc] initWithTargetClass:[User class]];
    NSXMLParser *parser = [[NSXMLParser alloc] initWithData:xmlData];
    parser.delegate = delegateImpl;
    [parser parse];
    
    id user = [delegateImpl resultObject];
    NSLog(@"name = %@",[user name]);

次に、処理の途中で必要となる情報を管理しておくためのクラスです。

「ElementInfo.h」
#import >Foundation/Foundation.h<
#import "PropertyInfo.h"

@interface ElementInfo : NSObject

//一つ下の階層の要素の値をセットする先となるオブジェクト
@property(strong,nonatomic) id target;

//要素名
@property(strong,nonatomic) NSString *elementName;

//要素に対応するプロパティの情報を管理するオブジェクト
@property(strong,nonatomic) PropertyInfo *propInfo;

@end

そして、次がデシリアライズ処理を行っている部分のソースです。

「ParserDelegateImpl.m」
#import "ParserDelegateImpl.h"
#import "ElementInfo.h"
#import "XmlEntity.h"
#import "PropertyInfo.h"

#import >objc/runtime.h<

@implementation ParserDelegateImpl{
    
    NSMutableArray *_elementStack;
    id _rootObject; //デシリアライズ結果のオブジェクト
}

-(id)initWithTargetClass:(Class)targetClass
{
    self = [super init];
    if(self){
        _elementStack = [NSMutableArray array];
        _rootObject = [[targetClass alloc] init];
    }
    return self;
}

-(void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI
      qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict
{
    
    //ルート要素の場合
    if([_elementStack count] == 0){
        //最上位階層の情報を管理するElementInfoを生成し、Stackにつめる
        ElementInfo *info = [[ElementInfo alloc] init];
        info.elementName = elementName;
        info.target = _rootObject;

        [_elementStack addObject:info];
    }
    //ルート要素以外の場合
    else{
        //親要素の情報を取り出す
        ElementInfo *parent = [_elementStack lastObject];
        //Userクラスから要素に対応するプロパティの情報を取得する
        PropertyInfo *propInfo =[[parent.target class] propertyInfoForElement:elementName];
        if(!propInfo){
            return;
        }
        //プロパティが存在しているかチェックする
        objc_property_t prop =  class_getProperty([parent.target class], [propInfo.name UTF8String]);
        if(prop){
            //要素を管理するElementInfoを生成し、Stackにつめる
            ElementInfo *info = [[ElementInfo alloc] init];
            info.elementName = elementName;
            info.propInfo = propInfo;
            
            //プロパティの型に応じて処理を分ける
            //NSStringの場合
            if(propInfo.type == [NSString class]){
                info.target = [[NSMutableString alloc] init];
            }
            
            [_elementStack addObject:info];
        }
    }     
}

-(void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI 
   qualifiedName:(NSString *)qName
{
    //スタックにつめられている最後のオブジェクトを取得し、その要素名が引数のelementNameと一致するか判定する
    ElementInfo *lastElement = [_elementStack lastObject];
    if([lastElement.elementName isEqualToString:elementName]){
        //targetオブジェクトがNSMutableStringの場合(テキストノードの場合)
        if([lastElement.target isKindOfClass:[NSMutableString class]]){
            //もう一つ上の階層のオブジェクトのプロパティにテキストノードの値を設定する
            ElementInfo *parentElement = [_elementStack objectAtIndex:[_elementStack count] -2];
            [parentElement.target setValue:lastElement.target forKey:lastElement.propInfo.name];
        }
    }
    
    [_elementStack removeLastObject];
}

-(void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string
{
    //stackの最後のオブジェクトがNSMutableStringの場合、テキストノードの値をそれにつめる
    ElementInfo *lastElement = [_elementStack lastObject];
    if([lastElement.target isKindOfClass:[NSMutableString class]]){
        [((NSMutableString *)lastElement.target) appendString:string];
    }
}

//解析結果のオブジェクトを返却する
-(id)resultObject
{
    return _rootObject;
}

@end


XMLの要素の開始を発見する毎に、その要素に対応するElementInfoクラスのインスタンスを生成し、
NSMutableArrayに追加していきます。要素の終了タグを発見すると、NSMutableArrayの最後のオブジェクトを
削除します。

これによって、NSMutableArrayには常に、現在処理中の要素の上位要素が管理されていることになります。
要するにNSMutableArrayをスタックとして使用しているわけです。

デシリアライズ処理の過程では常に現在の要素の上位要素に対応するオブジェクトへのアクセスが必要になります。
今回のサンプルでは、NameタグのテキストをUserタグに対応するUserクラスのインスタンスのプロパティに設定します。
これをスタックの仕組みを使って実現しているのです。

今回のソースではまだとてもシンプルな形をしたXMLしか扱うことができません。
そして、XMLに処理対象ではないXMLがあった場合の挙動も考慮出来ていません。
ですから、まだ実用レベルには達していないかと思います。
次回はもう少しその辺りを突き詰めてみます。


XMLからオブジェクトのデシリアライズをするiOS用のライブラリ「ChimeraXML(キメラXML)」をgithubに公開しました。
iOSのXMLデシリアライズライブラリ「ChimeraXML」

さて、前回の記事の続きですが、本稿では、Javaのアノテーションや、C#のアトリビュートのように
Objective-cのプロパティにXMLの要素名をメタ情報として、付与する方法を紹介します。

といってもObjective-cには、アノテーションの仕組みはありませんので、クラスメソッドを定義し、そのメソッドで
メタ情報を返す仕組みを考えてみました。

まず、プロパティのメタ情報を保持するクラスとして、PropertyInfoクラスを定義しました。

「PropertyInfo.h」
@interface PropertyInfo : NSObject

@property(strong,nonatomic) NSString *name;
@property(strong,nonatomic) Class type;

@end

プロパティ名と型を持つようにしました。
型はデシリアライズ処理をする際に必要になります。

次に、デシリアライズ先となるクラスの共通の親クラスとなる「XmlEntity」クラスを定義します。

「XmlEntityu.h」
@interface XmlEntity : NSObject

+(PropertyInfo *)propertyInfoForElement:(NSString *)element;

@end

要素名に対応するプロパティの情報を返すクラスメソッドを定義しています。

実装ファイルは以下になります。

「XmlEntityu.m」
@implementation XmlEntity

+(PropertyInfo *)propertyInfoForElement:(NSString *)element
{
    return nil;
}

@end

子クラスでそれぞれの要素名に対応するプロパティ情報を返すように実装するのでここでは、ただ、nilを返すようにしています。

そして、このクラスをデシリアライズ先となるUserクラスにXMLEntityクラスを継承させます。
その前に今回解析する対象のXMLを以下のように少し変更します。

前回のXMLでは、要素名とプロパティ名が完全一致するようにしていましたが、
今回は、一致しないようにしました。

<User>
  <Id>1</Id>
  <Name>山田太郎</Name>
  <Email>xxxxx@xxxx.com</Email>
</User>

Userクラスは以下のようになります。

「User.h」

@interface User : XmlEntity

@property(strong,nonatomic) NSString *userId;
@property(strong,nonatomic) NSString *name;
@property(strong,nonatomic) NSString *email;

@end

「User.m」
@implementation User

+(PropertyInfo *)propertyInfoForElement:(NSString *)element
{
    static NSDictionary *_propDic;
    
    if(!_propDic){
        
        PropertyInfo *userId = [[PropertyInfo alloc] init];
        userId.name = @"userId";
        userId.type = [NSString class];
        
        PropertyInfo *name = [[PropertyInfo alloc] init];
        name.name = @"name";
        name.type = [NSString class];
        
        PropertyInfo *email = [[PropertyInfo alloc] init];
        email.name = @"email";
        email.type = [NSString class];
        
        _propDic = @{
                     @"Id": userId,
                     @"Name": name,
                     @"Email" :email
                     };
    }
    
    return _propDic[element];
}
@end

「XMLEntity」クラスで定義された「propertyInfoForElement:element:」メソッドをオーバーライドし、
要素名に対応するUserクラスのプロパティ情報を返却するようにしました。

PropertyInfoのtypeは動的に取得することも可能ですが、ここでは一旦、手動で設定しています。

さて、これで、ようやく準備が整いましたので、次回は、デシリアライズする部分を紹介します。


XMLからオブジェクトのデシリアライズをするiOS用のライブラリ「ChimeraXML(キメラXML)」をgithubに公開しました。
iOSのXMLデシリアライズライブラリ「ChimeraXML」

Objective-cという言語はとても柔軟でかつ強力な言語であり、またC言語を内包しているため、
C言語で記述されたライブラリを境界なく使用できるという利点もあります。

しかし、不思議なことに、C#やJavaで提供されているXMLシリアライズ/デシリアライズ用のライブラリがなかなか
見つからないのです。
全く無いわけでは無いのですが、一般に広く認知されているライブラリが無いのです。

そのため、前回や前々回の記事で扱ったようにNSXMLParser等を使用してコツコツXMLを処理を書いて
いく事になってしまいます。

そこで本稿から数回に分けて、iOSでXMLからオブジェクトへのデシリアライズを試してみたいと思います。 まずは以下の簡単なXMLを使用します。

<User>
  <userId>1</userId>
  <name>山田太郎</name>
  <email>xxxxx@xxxx.com</email>
</User>

このXMLをデシリアライズする先のクラスが以下です。

@interface User : NSObject

@property(strong,nonatomic) NSString *userId;
@property(strong,nonatomic) NSString *name;
@property(strong,nonatomic) NSString *email;

@end

まず、デシリアライズを実現するために必要なことを洗い出してみたいと思います。
①Userという文字列からUserクラスのインスタンスが生成する
②XMLをパースしながら、Userクラスのインスタンスのプロパティに値を設定する。

①は、「NSClassFromString」を使用することで簡単に出来ます。

  Class userCls = NSClassFromString(@"User"); 
  User *user = [[userCls alloc] init];

②についてもObjective-cには非常に便利な仕組みが提供されています。
XMLをパースする部分は前回の記事で紹介していますので、
ここでは、タグ名とテキストノードの値がとれてからオブジェクトに値を設定する部分について紹介します。

方法は2通りあります。一つ目はKVCを使用する方法です。

  [user setValue:@"1" forKey:@"userId"];

KVCが何かという説明はまたどこかでさせて頂くとして、この方法は全ての型のオブジェクトに対して利用することが 可能です。
setValueの後に値を、forKeyの後にプロパティ名を記述するだけです。
ちなみに値の取得は

  [user valueForKey:@"userId"]);

になります。 2つ目の方法はperformSelectorを使用する方法です。

    if([user respondsToSelector:userIdSEL]){
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        [user performSelector:userIdSEL withObject:@"1"];
#pragma clang diagnostic pop
    }

プロパティのセッターをperformSelectorでコールする方法です。こちらの方法だとセッターが
存在しているか先にチェックすることができますので、今回の用途でいうと②の方が安全だと
思います。(厳密にいうと①でも、keyの存在チェックする方法はありますが、少し面倒なので)

「#pragma・・・」の部分は、「performSelector may cause a leak because its selector is unknown」
というコンパイラ警告がでるのを防止するためのおまじないです。ARCを利用していると、これがでてしまうようです。

さて、ここまでは順調ですね。iPhoneでもXMLのデシリアライズができそうです。
しかし、JavaやC#の経験がある方は既にお気づきかもしれませんが、
Objective-cにはJavaのアノテーションやC#のアトリビュートのようにメタデータを記述する方法が
無いので、要素名とプロパティ名は常に一定のルールに従って、命名しておく必要がでてきます。
例えば本稿のサンプルでは常に要素名とプロパティ名を一致させています。
他にも、例えば要素名にハイフンが入っている場合は、プロパティ名をキャメル形式にしておくとか
あるかもしれませんね。

ただ、サーバーサイドを自分たちで開発している場合は良いのですが、サードパーティのAPIを 使用する場合、これではかなり使いにくいですね。

これを解決するために、デシリアライズ先となるオブジェクトに共通の親クラスを設けて、
要素名に対応するプロパティ名を問い合わせすることができる仕組み(メソッドを設ける)を考えてみました。

長くなってきたので、それについては次回の記事で詳しくご紹介します。


前回の記事では[NSXMLParser]を使用して、XMLを解析する際の、基本的な部分を紹介させて頂きました。

本稿では具体的なXMLを使って、XMLを解析する際のテクニックをご紹介したいと思います。

サンプルのXMLは以下を使用します。

<User>
  <Id>1</Id>
  <Name>山田太郎</Name>
  <Email>xxxxx@xxxx.com</Email>
  <Friends>
    <User>
      <Id>2</Id>
    </User>
    <User>
      <Id>3</Id>
    </User>
  </Friends>
</User>

Objective-cのコードは以下です。


-(void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI
      qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict
{
    _tagName = elementName;
}

-(void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI
    qualifiedName:(NSString *)qName
{
    _tagName = nil;
}

-(void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string
{
    if([_tagName isEqualToString:@"Name"]){
        NSLog(@"Name is %@",string);
    }
}


テキスト部分を処理する「(void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string」にはタグ名は渡されませんので、 開始タグを処理するメソッドで、インスタンス変数[_tagName]に要素名をセットし、それを利用して処理したいタグを判別するようにしています。

NSXMLParserを使用する際に一点気をつけないといけないことがあります。NSXMLParserでは1つのテキストノードの文字列を処理する間に、「foundCharacters」が 複数回に分けてコールされることがあります。これを知らずに処理を書いていると文字列が途中で切れてしまうことになります。
それに対応するコードが以下になります。

@implementation APIResultSearializer{
    NSMutableString __strong *_stringBuffer;
}

-(void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI
    qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict
{
    _tagName = elementName;
}

-(void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string
{
    if(!_stringBuffer){
        _stringBuffer = [[NSMutableString alloc] init];
    }
    [_stringBuffer appendString:string];
}

-(void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI
     qualifiedName:(NSString *)qName
{    
    if(_stringBuffer){
        if([_tagName isEqualToString:@"Name"]){
            NSLog(@"Name is %@ ",_stringBuffer);         
        }
        _stringBuffer = nil;
    }    
    _tagName = nil;
}


NSMutableStringを使用して、そこにテキストを積めておいて、要素の終了タグのハンドラがコールされた時点でそれを取り出して処理します。

ここまでで、XMLから必要な要素の値を取得する処理はとりあえず完了なのですが、一点問題があります。
サンプルのXMLには<Id>1</Id>がUser直下とUser/Friends/Userの下の2カ所に存在しているため、
上記のコードのようにタグ名だけで処理をしているとどちらか片方を取りたい時に問題が発生してしまいます。

それに対応するコードが以下になります。

@implementation ParserDelegateImpl{    
    NSMutableArray *_elementStack;
    NSMutableString __strong *_stringBuffer;
}

-(id)init
{
    self = [super init];
    if(self){
        _elementStack = [NSMutableArray array];
    }
    return self;
}

-(void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI 
   qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict
{
     [_elementStack addObject:elementName];
}

-(void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI
    qualifiedName:(NSString *)qName
{    
    if(_stringBuffer){
        if([[_elementStack lastObject] isEqualToString:@"Id"] && [_elementStack count] == 2){
            NSLog(@"Id is %@ ",_stringBuffer);
        }
        else if([[_elementStack lastObject] isEqualToString:@"Id"] && [_elementStack count] == 4){
            NSLog(@"User/Friends/User/Id is %@ ",_stringBuffer);
        }
        _stringBuffer = nil;
    }
    
    [_elementStack removeLastObject];
}

-(void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string
{
    if(!_stringBuffer){
        _stringBuffer = [[NSMutableString alloc] init];
    }
    [_stringBuffer appendString:string];
}

NSMutableArrayをスタックとして使用し、開始タグでスタックにつめて、終了タグでスタックから削除するようにします。 上記のサンプルのようにスタックのサイズだけで判定するのであればint型変数を開始/終了タグのタイミングでプラスマイナスすることでも 対応可能ですが、より複雑なXMLを処理する場合は、この方法が役に立つと思います。

NSXMLParserでXMLを解析する(1)

XMLからオブジェクトのデシリアライズをするiOS用のライブラリ「ChimeraXML(キメラXML)」をgithubに公開しました。
iOSのXMLデシリアライズライブラリ「ChimeraXML」

とても長い間を記事を書くのをお休みしていたのですが、また少しずつですが、再開したいと思います。
今後ともぜひよろしくお願いします。

以前はC#の話題を中心に記事を書いていたのですが、今後はブログ名を「気楽なソフト工房」と改名し、 スマートフォンアプリとかWebとかもう少し幅広い分野の記事を扱っていきたいと思います。

再開後、初回の今日は、iPhoneでXMLを扱う際の方法について書いてみたいと思います。
iOSでXMLを扱う場合、iOSが提供している標準のライブラリである「NSXMLParser」や「libxml2」を使用する方法や、または オープンソースで公開されているサードパーティのライブラリを使用する方法があります。 このサイトに色々なライブラリが詳しく紹介されています。

本稿ではiOS標準のSAXのライブラリ「NSXMLParser」を使用する方法をご紹介します。
SAXは一般的にDOMよりメモリ使用量は少ないのですが、低速だと言われています。(英語のサイトですが、ここに 様々なライブラリのパフォーマンス検証の結果が記載されています。)
また、「NSXMLParser」はC言語のライブラリである「libxml2」をObjective-cでラップしているため、それを比較するとさらに低速になります。

ここまでの話を聞いてしまうと「NSXMLParser」で良いのか?と考えてしまうかもしれませんが、私は大抵のケースにおいて問題ないのでは無いかと考えています。
例えば、検索結果1000件のXMLを扱う場合、パフォーマンスに不安無いのかと聞かれるとそれは不安になってしまいます。
ただ、この場合では、例えば50件ずつ表示する仕様にするなどして、別の方法で問題を回避できますし、実際はそういうケースが多いのではと思います。
つまり常識的な範囲で使用している限りはそんなに問題は無いと思っています。

さて、ここからNSXMLParserの使用方法を簡単にご紹介していきます。NSXMLParserを使用するためには以下の2つの事を行う必要があります。
①「NSXMLParserDelegate」プロトコルを採用したクラスを作成する。
②①で作成したクラスをNSXMLParserのdelegateに設定し、パースを実行する。

SAXはイベント駆動で、XMLを上から下に走査しながら、要素の開始タグなどを発見したタイミングで
そのイベントを処理するクラスの所定のメソッドを呼び出すような形になっています。

NSXMLParserの場合、そのイベントを処理クラスが「NSXMLParserDelegate」プロトコルを採用したクラスということになります。

[ParserDelegateImpl.h]
#import <Foundation/Foundation.h>

@interface ParserDelegateImpl : NSObject<NSXMLParserDelegate>

@end

[ParserDelegateImpl.m]
@implementation ParserDelegateImpl

//開始タグ
-(void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI 
      qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict
{
    NSLog(@"didStartElment = %@",elementName); 
}

//終了タグ
-(void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI 
      qualifiedName:(NSString *)qName
{
    NSLog(@"didEndElment = %@",elementName);
}

//テキストノード
-(void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string
{
    NSLog(@"foundCharacters = %@",string);
}

上記のサンプルコードでは、よく使うメソッドのみを実装していますが、他にも多数メソッドが定義されています。
全てoptionalで定義されているので、必要なものだけ、実装しておけば問題ありません。
次にパースを実行する部分のソースを紹介します。

    NSString *xml = @"<User><Name>太郎</Name></User>"
	
    //NSStringをNSDataに変換する
    NSData *xmlData = [xml dataUsingEncoding:NSUTF8StringEncoding];
	
    //パーサーのインスタンスを生成し、デリゲートを設定する
    ParserDelegateImpl *delegateImpl = [[ParserDelegateImpl alloc] init];
    NSXMLParser *parser = [[NSXMLParser alloc] initWithData:xmlData];
    parser.delegate = delegateImpl;
	
    //パースを実行する
    [parser parse];

NSXMLParserはNSStringを扱うイニシャライザを持っていないので、NSDataに変換してから解析を行う必要があります。
駆け足で簡単にNSXMLParserの使いかたを紹介しましたが、次回はもう少し解析する部分の処理を詳しくご紹介したいと思います。

NSXMLParserでXMLを解析する(2)

上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。