Cocoa練習帳 -27ページ目

[iOS]CoreAnimation (レイヤ)

毎度、知らない事を題材にしているので内容に自信はないのだが、今回は、特に自信がない。理解していない部分があるが、前に進む為に挑戦してみる。




今まで著者は、主に通常のビューに対する描画を扱ってきたが、今回はレイヤを使った描画に挑戦だ。




生成した画像やレイヤを管理する為のプロパティを用意した。




@interface LayerViewController : UIViewController
 
@property (strong, nonatomic) UIImage   *backgroundImage;
@property (strong, nonatomic) UIImage   *frontImage;
@property (strong, nonatomic) UIImage   *rearImage;
@property (strong, nonatomic) CALayer   *cardLayer;
@property (strong, nonatomic) CALayer   *frontLayer;
@property (strong, nonatomic) CALayer   *rearLayer;
 
@end



画面が表示される前に、背景とトランプ・カードのレイヤを追加。




@implementation LayerViewController
 
@synthesize backgroundImage = _backgroundImage;
@synthesize frontImage = _frontImage;
@synthesize rearImage = _rearImage;
@synthesize cardLayer = _cardLayer;
@synthesize frontLayer = _frontLayer;
@synthesize rearLayer = _rearLayer;
 
...


- (void)viewWillAppear:(BOOL)animated
{
    DBGMSG(@"%s", __func__);
    
    /* 背景レイヤに背景画像を設定 */
    self.backgroundImage = [UIImage imageNamed:@"background.png"];
    self.view.layer.contents = (id)self.backgroundImage.CGImage;
    
    /* カードの表と裏の画像を用意 */
    self.frontImage = [UIImage imageNamed:@"front.png"];
    self.rearImage = [UIImage imageNamed:@"rear.png"];
    
    /* 表面レイヤと裏面レイヤを一塊のカードとして扱う為のレイヤ */
    CATransform3D   perspactive = CATransform3DIdentity;
    perspactive.m34 = -1.0 / 100.0; /* 遠近感をつける */
    self.cardLayer = [CALayer layer];
    self.cardLayer.bounds = CGRectMake(0.0, 0.0, 100.0, 100.0);
    self.cardLayer.position = CGPointMake(100.0, 100.0);
    self.cardLayer.sublayerTransform = perspactive;
    self.cardLayer.name = @"card";
    
    /* 表面レイヤ */
    self.frontLayer = [CALayer layer];
    self.frontLayer.bounds = CGRectMake(0.0, 0.0, 100.0, 100.0);
    self.frontLayer.position = CGPointMake(50.0, 50.0);
    self.frontLayer.contents = (id)self.frontImage.CGImage;
    self.frontLayer.zPosition = 1; /* 表面レイヤを手前に配置 */
    self.frontLayer.name = @"front";
    
    /* 裏面レイヤ */
    self.rearLayer = [CALayer layer];
    self.rearLayer.bounds = CGRectMake(0.0, 0.0, 100.0, 100.0);
    self.rearLayer.position = CGPointMake(50.0, 50.0);
    self.rearLayer.contents = (id)self.rearImage.CGImage;
    self.rearLayer.zPosition = 0;
    self.rearLayer.transform = CATransform3DMakeRotation(M_PI, 0.0, 1.0, 0.0); /* 裏返す */
    self.rearLayer.name = @"rear";
    
    /* 表面レイヤと裏面レイヤをカード・レイヤのサブ・レイヤに設定 */
    [self.cardLayer addSublayer:self.frontLayer];
    [self.cardLayer addSublayer:self.rearLayer];
    
    /* カード・レイヤを背景レイヤのサブ・レイヤに設定 */
    [self.view.layer addSublayer:self.cardLayer];
}
...
@end



裏面のレイヤは裏返して、表面のレイヤと張り合わせている感じだ。




効率は悪いと思うが、レイヤの追加と削除の流れを確認する為、非表示になった際にレイヤを削っている。




- (void)viewDidDisappear:(BOOL)animated
{
    DBGMSG(@"%s", __func__);
    [self.frontLayer removeFromSuperlayer];
    [self.rearLayer removeFromSuperlayer];
    [self.cardLayer removeFromSuperlayer];
    self.frontLayer = nil;
    self.rearLayer = nil;
    self.cardLayer = nil;
    self.frontImage = nil;
    self.rearImage = nil;
    self.view.layer.contents = nil;
    self.backgroundImage = nil;
}



トランプ・カードがタッチされたら反転させる。




- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    DBGMSG(@"%s", __func__);
    UITouch *touch = [touches anyObject];
    CGPoint position = [touch locationInView:self.view];
    CALayer *layer = [self.view.layer hitTest:position]; /* 触ったレイヤ */
    CALayer *containerLayer = layer.superlayer; /* 親レイヤ */
    
    DBGMSG(@"layer name: %@", layer.name);
    DBGMSG(@"container layer name: %@", containerLayer.name);
    
    /* 親レイヤはカード・レイヤ? */
    if ([containerLayer.name hasPrefix:@"card"]) {
        DBGMSG(@"container layer is card");
        containerLayer.zPosition = 10;
        [CATransaction begin];
        [CATransaction setAnimationDuration:1.0];
        CATransform3D   transform = CATransform3DIdentity;
        transform.m34 = -1.0 / 100.0; /* 遠近感をつける */
        
        /* 表に戻す */
        if ([containerLayer.name hasSuffix:@"flipped"]) {
            /* 裏返っていたら、名前の末尾に.flippedがついている */
            transform = CATransform3DRotate(transform, 0.0, 0.0, 1.0, 0.0);
            containerLayer.name = [containerLayer.name stringByDeletingPathExtension];
            /* 名前の末尾の.flippedを削る */
        }
        /* 裏返す */
        else {
            transform = CATransform3DRotate(transform, - M_PI, 0.0, 1.0, 0.0);
            containerLayer.name = [containerLayer.name stringByAppendingPathExtension:@"flipped"];
            /* 裏返したら、名前の末尾に.flippedがつける */
        }
        
        containerLayer.sublayerTransform = transform;
        [CATransaction commit];
    }
}



実行。

layer







ソースコード

GitHubからどうぞ。

https://github.com/murakami/workbook/tree/master/ios/CoreAnimation - GitHub




関連情報

Core Animation for Mac OS X and the iPhone

Creating Compelling Dynamic User Interfaces

Core Animationプログラミングガイド

アニメーションのタイプとタイミング

iOS Developer Libraryの翻訳文書だ。

15歳からはじめるiPhoneわくわくゲームプログラミング教室

分かりやすく無駄がない説明。

OpenGLの神髄

CoreAnimatonの理解の助けになった。

iPhoneソーシャルゲーム開発

理解不足の箇所があるので参考になった。

Core Animation for Max OS X and the iPhone

Creating Compelling Dynamic User Interfaces

[iOS]位置情報と住所

位置情報から住所を取得するのに挑戦だ。


iOS 5からMKReverseGeoCoderの利用は推奨されず、かわりに、CLGeocoderの利用が推奨されるようになった。CLGeocoderはBlocksを使ったモダンなクラスなので、この変更は歓迎すべきではなるのだが、問題は、情報が少ない。リファレンスの説明もよく分からない。そこで、試行錯誤しながら、進めてゆく。




以前の位置情報を取得するメソッドで、住所を取得するコードを追加する。




    self.geocoder = [[CLGeocoder alloc] init];
...
- (void)locationManager:(CLLocationManager *)manager
    didUpdateToLocation:(CLLocation *)newLocation
           fromLocation:(CLLocation *)oldLocation
{
    [self.locationManager stopUpdatingLocation];
    
    GPXTrackPoint   *trkpt = nil;
    trkpt = [self.document.gpxTrack newTrackpointWithLatitude:newLocation.coordinate.latitude
                                                    longitude:newLocation.coordinate.longitude];
    trkpt.time = newLocation.timestamp;
    
    NSString    *s = [[NSString alloc] initWithFormat:@"(%f, %f)",
                      newLocation.coordinate.latitude,
                      newLocation.coordinate.longitude];
    self.messageLabel.text = s;
    
    [self.geocoder reverseGeocodeLocation:newLocation completionHandler:
        ^(NSArray* placemarks, NSError* error) {
            住所を取得
        }];
}



ここで問題なのは、ブロックの引数のplacemarksの内容がよく分からない。placemarksはCLPlacemarkの配列という情報を得て、CLPlacemarkにはaddressDictionaryというプロパティがあるので、それをダンプしてみた。




[self.geocoder reverseGeocodeLocation:newLocation completionHandler:
        ^(NSArray* placemarks, NSError* error) {
            if(!error){
                for(CLPlacemark *placemark in placemarks){
                    for (NSString *key in placemark.addressDictionary.allKeys) {
                        NSLog(@"Key: %@", key);
                    }
                }
            }
        }];



以下がその結果だ。




2012-05-08 20:54:44.188 WayPoints[7456:f803] Key: FormattedAddressLines
2012-05-08 20:54:44.189 WayPoints[7456:f803] Key: Street
2012-05-08 20:54:44.190 WayPoints[7456:f803] Key: SubAdministrativeArea
2012-05-08 20:54:44.191 WayPoints[7456:f803] Key: Thoroughfare
2012-05-08 20:54:44.192 WayPoints[7456:f803] Key: ZIP
2012-05-08 20:54:44.193 WayPoints[7456:f803] Key: Name
2012-05-08 20:54:44.195 WayPoints[7456:f803] Key: City
2012-05-08 20:54:44.196 WayPoints[7456:f803] Key: PostCodeExtension
2012-05-08 20:54:44.197 WayPoints[7456:f803] Key: Country
2012-05-08 20:54:44.198 WayPoints[7456:f803] Key: State
2012-05-08 20:54:44.198 WayPoints[7456:f803] Key: SubLocality
2012-05-08 20:54:44.199 WayPoints[7456:f803] Key: SubThoroughfare
2012-05-08 20:54:44.201 WayPoints[7456:f803] Key: CountryCode



このキーのFormattedAddressLinesで指されるのが、NSStringの配列のようで、これをダンプしてみた。




[self.geocoder reverseGeocodeLocation:newLocation completionHandler:        ^(NSArray* placemarks, NSError* error) {
            NSMutableString *str = [NSMutableString stringWithString:@""];
            if (!error) {
                for (CLPlacemark *placemark in placemarks) {
                    for (NSString *key in placemark.addressDictionary.allKeys) {
                        NSLog(@"Key: %@", key);
                    }
                    NSArray *array = [placemark.addressDictionary objectForKey:@"FormattedAddressLines"];
                    for (NSString *line in array) {
                        [str appendString:line];
                        [str appendString:@", "];
                    }
                    [str appendString:@"\n"];
                }
            }
            else {
                str = [NSString stringWithFormat:@"error: %@", error];
            }
            NSLog(@"%@", str);
            self.gpxTextView.text = str;
        }];

run


ソースコード

GitHubからどうぞ。

https://github.com/murakami/workbook/tree/master/ios/WayPoints - GitHub




関連情報

iOS GPX Framework

GitHub

This is a iOS framework for parsing/generating GPX files. This Framework parses the GPX from a URL or Strings and create Objective-C Instances of GPX structure.

iOSプログラミング逆引きリファレンス108 ~知りたいことがすぐわかるiPhoneプログラミングテクニック~

iPhoneアプリ開発 熟達テクニック

上記、2冊には助けられた。

[iOS]GPSとGPX(その3)

前回からの変更点。GPX関連のクラスのインスタンスをDocumentクラスで管理するように変更。CoreLocationフレームワークを追加して、CoreLocation/CoreLocation.h をインポートする。




ViewControllerクラスのヘッダーにCLLocationManagerDelegateをプロトコルとして設定して、CoreLocation関連のインスタンスを追加。




@interface ViewController : UIViewController >CLLocationManagerDelegate<
 
@property (strong, nonatomic) IBOutlet UILabel      *messageLabel;
@property (strong, nonatomic) IBOutlet UITextView   *gpxTextView;
@property (strong, nonatomic) Document              *document;
@property (strong, nonatomic) CLLocationManager     *locationManager;
 
- (IBAction)trackPoint:(id)sender;
- (IBAction)dump:(id)sender;
@end



track pointボタンが押下され、- (IBAction)trackPoint:(id)senderが呼ばれたら、現在の位置情報をGPXデータに軌跡として登録するコードを追加する。




- (void)viewDidLoad
{
    [super viewDidLoad];
    
    AppDelegate


    
    self.locationManager = [[CLLocationManager alloc] init];
    self.locationManager.delegate = self;
}
 
- (IBAction)trackPoint:(id)sender
{
    [self.locationManager startUpdatingLocation];
}
 
- (void)locationManager:(CLLocationManager *)manager
    didUpdateToLocation:(CLLocation *)newLocation
           fromLocation:(CLLocation *)oldLocation
{
    [self.locationManager stopUpdatingLocation];
    
    GPXTrackPoint   *trkpt = nil;
    trkpt = [self.document.gpxTrack newTrackpointWithLatitude:newLocation.coordinate.latitude
                                                    longitude:newLocation.coordinate.longitude];
    trkpt.time = newLocation.timestamp;
}


- (void)locationManager:(CLLocationManager *)manager
       didFailWithError:(NSError *)error
{
    [self.locationManager stopUpdatingLocation];
}



これだと、どんなGPXデータが生成されているのか分からないので、ダンプする機能を追加。




- (IBAction)dump:(id)sender
{
    self.gpxTextView.text = self.document.gpxRoot.gpx;
}



これは、実機でないと上手く試せないと思う。

run








ソースコード

GitHubからどうぞ。

https://github.com/murakami/workbook/tree/master/ios/WayPoints - GitHub




関連情報

iOS GPX Framework

GitHub

This is a iOS framework for parsing/generating GPX files. This Framework parses the GPX from a URL or Strings and create Objective-C Instances of GPX structure.

iOSプログラミング逆引きリファレンス108 ~知りたいことがすぐわかるiPhoneプログラミングテクニック~

iPhoneアプリ開発 熟達テクニック

上記、2冊には助けられた。

[iOS]Push Notification(メッセージ通知)

今回も動作確認はまだで、話のみ。詳しくは、『iPhoneアプリ開発 熟達テクニック(林晃 著)』で確認して欲しい。また、Appleの文書の説明もいいので、こちらでも確認して欲しい。




通知する電文には、単純形式と拡張形式の2種類があるようだが、サンプル・コードは単純形式を利用している。


電文の形式を図にすると以下のとおり。

通知






通知先のデバイスが複数ある場合は、その個数分書き込むことになるが、APNs側にデバイスのリストと電文を渡して、後は宜しくとしてしまうと、APNs側の負担が高くなるので、独自サーバ側で宛先分書き込ませているのか?また、同一セッションで複数個書き込むのが、APNs側にとって、やさしい対応ということになるのだろう。




アプリケーションの通知を受ける側で呼ばれるメソッドは、Local NotificationとPush Notificationで異なる部分があるようだ。




Push Notificationについては、動作確認できる、公開可能なコードが用意できたら、もう少し踏み込んで発表したいと思っている。




ソースコード
GitHubからどうぞ。

https://github.com/murakami/workbook/tree/master/ios/PushNotification - GitHub


関連情報
iPhoneアプリ開発 熟達テクニック

周辺のサーバ関連の説明を丁寧で、かつ、ストレートに説明されていて、とても参考になりました。

Local および Push Notification プログラミングガイド

Developerサイトの情報。

[iOS]Push Notification(機器登録)

以前のLocal Notificationにつづいて、今回は、Push Notificationだ。


ただし、Push Notificationは、動作確認する場合は、iOS Provisioning PortalでApp IDを登録したり、サーバを用意したりと準備が必要なため、申し訳ないが、動作確認を行っていない内容だ。


また、『iPhoneアプリ開発 熟達テクニック(林晃 著)』の内容をかなり参考している為、掲載するコードも参考程度とした。詳しくは、是非、この書籍を購入して内容を確認して欲しい。




外部サーバから情報を取得する方法として、iOS機器側でポーリングしてしまうとバッテリーの消費が問題となったり、無駄な通信が発生してします。その為、Push Notificationでは、外部からの通知とトリガーにiOSアプリケーションが対応する方法をとっている。ただし、この部分を無制限に公開してしまうと、セキュリティ上の問題となる。でも、何から何までAppleのサーバで行うと、Apple側の負担となると予想される。


そこで、Apple Push Notificationサービス(APNs)というサービスを用意して、APNsが発行したデバイス・トークンに対して、サードパーティ側の独自サーバがAPNsを経由して通知を送るという仕組みになっている。




今回は、このデバイス・トークンを取得する部分を試してみた。




これを図にすると以下のとおり。

機器登録








デバイス・トークンは、iOSアプリケーションがAPNsに対して取得を行い、得られたデバイス・トークンをアプリケーションが独自サーバに渡して、独自サーバはそれを覚えておくという流れになっている。


アプリケーションとAPNs間の通信は、フレームワークのメソッドを通じて行われている為、アプリケーションは、ネットワーク通信を行っている事を意識しない方法になっている。




ソースコード
GitHubからどうぞ。

https://github.com/murakami/workbook/tree/master/ios/PushNotification - GitHub


関連情報
iPhoneアプリ開発 熟達テクニック

周辺のサーバ関連の説明を丁寧で、かつ、ストレートに説明されていて、とても参考になりました。

Local および Push Notification プログラミングガイド

Developerサイトの情報。

[iOS]ハイパーリンク

Twitterの詳細画面のように。テキスト中のハイパーリンクの文字色等を変更して、選ばれたら、そのURLをブラウザで開くようにするにはどうすればいいのか?詳しく調べていないが、一つはテキストをHTML化して、UIWebViewで表示するという案を思い浮かんだ。


OS Xの場合は、Technical Q&A QA1487「Embedding Hyperlinks in NSTextField and NSTextView」で説明されている。


iOSの場合、NSString関連のメソッドに差がある為、OS Xと同様な方法が適用できるのか分からない為、試行錯誤してみた。




そもそも、OS Xの場合は、どうするのか確認してみる。以下は、QA1487のコードそのままだ。




/* NSAttributedStringを拡張するカテゴリとして実装 */
@interface NSAttributedString (Hyperlink)
    +(id)hyperlinkFromString:(NSString*)inString withURL:(NSURL*)aURL;
@end
 
@implementation NSAttributedString (Hyperlink)
/*
 * NSTextFieldの場合、属性と選択を可能にしておく必要がある。
 * [テキストフィールド setAllowsEditingTextAttributes:YES];
 * [テキストフィールド setSelectable:YES]
 */
+(id)hyperlinkFromString:(NSString*)inString withURL:(NSURL*)aURL
{
    NSMutableAttributedString* attrString = [[NSMutableAttributedString alloc] initWithString: inString];
    NSRange range = NSMakeRange(0, [attrString length]);
 
    [attrString beginEditing];
 
    /* ハイパーリンクを設定 */
    [attrString addAttribute:NSLinkAttributeName value:[aURL absoluteString] range:range];
 
    // make the text appear in blue
    [attrString addAttribute:NSForegroundColorAttributeName value:[NSColor blueColor] range:range];
 
    // next make the text appear with an underline
    [attrString addAttribute:
            NSUnderlineStyleAttributeName value:[NSNumber numberWithInt:NSSingleUnderlineStyle] range:range];
 
    [attrString endEditing];
 
    return [attrString autorelease];
}
@end



iOSの場合になるが、iOS 3.2以降、NSRegularExpressionSearchで正規表現が扱えるようになったので、これでURLを見つけて、上記のような方法でハイパーリンクを設定すればいいのか考えてた。


が、iOSの場合、以外と安易な解決策を見つけてしまった。




iOS 3以降、UIDataDetectorTypesのUIDataDetectorTypeLinkを指定すれば、リンクを作成できる。試してみよう。




self.textView.editable = NO;
self.textView.dataDetectorTypes = UIDataDetectorTypeLink;
self.textView.text = @"This is a demonstration.\nhttp://www.bitz.co.jp/\nThank you.";



あっけなかった。簡単だ。




ただ、この場合は、URLを開く流れを制御できない。例えば、ちょっとしたアプリケーション固有の情報を追加するとか、Safariでなく自身のUIWebViewに表示するとか。




URLを開くのは、UIApplicationの- openURL:メソッドの呼び出しによってなので、このメソッドを捕まえて、差し替えれば良いのでは?




- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.openURLMethod = class_getInstanceMethod([UIApplication class], @selector(openURL:));
    Method  myOpenURLMethod = class_getInstanceMethod([AppDelegate class], @selector(myOpenURL:));
    method_exchangeImplementations(self.openURLMethod,  myOpenURLMethod);
    return YES;
}
 
- (BOOL)myOpenURL:(NSURL *)url
{
    NSLog(@"%s, url(%@)", __func__, url);
    return YES;
}



うまく捕まえる事はできた。ただ、どうすれば、オリジナルの- openURL:メソッドを呼んでいいのか分からなかったり、出来ても、変数のアクセス等、素直に実装できそうにないので、これは諦める事にした。




結局、UIApplicationのサブクラスを作成して、- opneURL:をオーバーライドすることにした。




@interface MyApplication : UIApplication
@end
    ....
@implementation MyApplication
- (BOOL)openURL:(NSURL *)url
{
    NSLog(@"%s, url(%@)", __func__, url);
    return [super openURL:url];
}
@end



main.mを変更して、この独自のサブクラスが呼ばれるようにする。




int main(int argc, char *argv[])
{
    @autoreleasepool {
        return UIApplicationMain(argc, argv,
            NSStringFromClass([MyApplication class]),
            NSStringFromClass([AppDelegate class]));
    }
}



UIApplicationMainの第三引数がnilになっていたと思うが、そこにMyApplcationを設定する。




ソースコード
GitHubからどうぞ。

https://github.com/murakami/workbook/tree/master/ios/Hyperlinks - GitHub


関連情報
Technical Q&A QA1487

Embedding Hyperlinks in NSTextField and NSTextView

Technical Q&A QA1629

Launching the App Store from an iPhone application

Text, Web, and Editing Programming Guide for iOS

Developerサイトの情報。

[iOS]Tweeting(アカウント管理)

独自にOAuth/xAuthに対応する場合は、アプリケーションはアカウントに対応したアクセストークンを取得して、これを使ってアクセスすることになる。


iOS5から用意されたTwitter/Accounts frameworkを利用する場合、そもそも管理されているTwitterアカウントが複数あり、ユーザがそれのどれを選択したのか管理する必要がある。


それが、ACAccountのプロパティidentifierだ。




- (IBAction)tweet2:(id)sender
{
    ACAccountStore *accountStore = [[ACAccountStore alloc] init];

    ACAccountType *accountType = [accountStore accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierTwitter];

    [accountStore requestAccessToAccountsWithType:accountType
            withCompletionHandler:^(BOOL granted, NSError *error) {
        if(granted) {
            NSArray *accountsArray = [accountStore accountsWithAccountType:accountType];

            for (NSUInteger i = 0; i < [accountsArray count]; i++) {
                ACAccount *twitterAccount = [accountsArray objectAtIndex:i];
                NSLog(@"account: %@", twitterAccount);

                TWRequest *postRequest = [[TWRequest alloc]
                    initWithURL:
                    [NSURL URLWithString:@"http://api.twitter.com/1/statuses/update.json"]
                    parameters:[NSDictionary dictionaryWithObject:@"hello, world" forKey:@"status"]
                    requestMethod:TWRequestMethodPOST];

                [postRequest setAccount:twitterAccount];
                
                NSLog(@"credential: %@", twitterAccount.credential);
                NSLog(@"identifier: %@", twitterAccount.identifier);

                [postRequest performRequestWithHandler:
                        ^(NSData *responseData, NSHTTPURLResponse *urlResponse, NSError *error) {
                    NSString *output = [NSString stringWithFormat:@"HTTP response status: %i",
                        [urlResponse statusCode]];
                    NSLog(@"%@", output);
                    [self performSelectorOnMainThread:@selector(displayText:)
                        withObject:output waitUntilDone:NO];
                }];
            }
        }

}



ACAccountのプロパティidentifierを保存しておけば、ACAccountStoreクラスの - (ACAccount *)accountWithIdentifier:(NSString *)identifier メソッドを使えば、ACAcountクラスのインスタンスを得られる。




ソースコード
GitHubからどうぞ。

https://github.com/murakami/workbook/tree/master/ios/Tweets - GitHub


関連情報
iOS Twitter framework

Twitter Developersサイトの情報。

Tweeting

Developerサイトのサンプル・コード

[iOS][Web]iPhoneアプリケーションとサーバ間の通信(その4)

先日のRESTの回では、取得したXMLデータを解析していなかったので、今回は、解析に挑戦だ。




XMLデータへのアクセス方法には、DOMとSAXの2種類があるが、iOSではCocoaのフレームワークで用意されているのはSAXに対応するNSXMLParserのみだ。もちろん、DOMも利用できるか、Cocoaのフレームワークなので手軽だという事と、iOS機器の貧弱なリソース(メモリetc)を考えて、NSXMLParserを利用する方法を紹介する。




ビー・コントローラーにNSXMLParserDelegateプロトコルを追加し、解析で使用するプロパティを追加する。




@interface ViewController : UIViewController <NSXMLParserDelegate>

@property (assign, nonatomic) BOOL                  inPersonElement;
@property (assign, nonatomic) BOOL                  inNameElement;
@property (assign, nonatomic) BOOL                  inAgeElement;
@property (strong, nonatomic) NSMutableString       *name;
@property (strong, nonatomic) NSMutableString       *age;

@end



返ってきたデータdataをパースする。ARCのおかげで、記述が簡素になっている。




        if (data) {
            NSXMLParser *xmlParser = [[NSXMLParser alloc] initWithData:data];
            xmlParser.delegate = self;
            [xmlParser parse];
        }



パースで利用する、委任メソッドだ。




- (void)parserDidStartDocument:(NSXMLParser *)parser
{
    self.inPersonElement = NO;
    self.inNameElement = NO;
    self.inAgeElement = NO;
}
 
- (void)parserDidEndDocument:(NSXMLParser *)parser
{
}
 
- (void)parser:(NSXMLParser *)parser
    didStartElement:(NSString *)elementName
    namespaceURI:(NSString *)namespaceURI
    qualifiedName:(NSString *)qualifiedName
    attributes:(NSDictionary *)attributeDict
{
    if ([elementName isEqualToString:@"person"]) {
        self.inPersonElement = YES;
    }
    else if ([elementName isEqualToString:@"name"]) {
        self.inNameElement = YES;
        self.name = [[NSMutableString alloc] init];
    }
    else if ([elementName isEqualToString:@"age"]) {
        self.inAgeElement = YES;
        self.age = [[NSMutableString alloc] init];
    }
}
 
- (void)parser:(NSXMLParser *)parser
    didEndElement:(NSString *)elementName
    namespaceURI:(NSString *)namespaceURI
    qualifiedName:(NSString *)qName
{
    if ([elementName isEqualToString:@"person"]) {
        self.inPersonElement = NO;
        NSLog(@"person(name[%@], age[%@])", self.name, self.age);
    }
    else if ([elementName isEqualToString:@"name"]) {
        self.inNameElement = NO;
    }
    else if ([elementName isEqualToString:@"age"]) {
        self.inAgeElement = NO;
    }
}
 
- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string
{
    if (self.inPersonElement) {
    }
     
    if (self.inNameElement) {
        [self.name appendString:string];
    }
    else if (self.inAgeElement) {
        [self.age appendString:string];
    }
}



NSXMLParserで解析する場合、ステータスを覚えておいて、ステータスに合わせて得られた情報を扱う必要があるが、この程度の簡素なデータでは、特に問題はないと思う。




ソースコード
GitHubからどうぞ。

https://github.com/murakami/workbook/tree/master/ios/REST - GitHub


関連情報
CHAPTER 5 Representational State Transfer (REST)

Fieldingの論文。

iOSプログラミング逆引きリファレンス108 ~知りたいことがすぐわかるiPhoneプログラミングテクニック~

全面的に、この書籍を参考にしています。助かります!

URL Loading System Programming Guide

Event-Driven XML Programming Guide

Developerサイトの情報。

[iOS][Web]iPhoneアプリケーションとサーバ間の通信(その3)

おそらく、Twitterフレームワークの為だと思うが、iOS5からNSJSONSerializationというJSONを扱うクラスが追加された。


このクラスは非常にシンプルで、用意されているメソッドは以下の5つだけだ。

+ JSONObjectWithData:options:error:
JSONデータを配列(NSArray)/辞書(NSDictionary)に変換して返す。
+ JSONObjectWithStream:options:error:error
ストリームから得られたJSONデータを配列(NSArray)/辞書(NSDictionary)に変換して返す。
+ dataWithJSONObject:options:error:
配列(NSArray)/辞書(NSDictionary)をJSONデータに変換して返す。
+ writeJSONObject:toStream:options:error:
配列(NSArray)/辞書(NSDictionary)をJSONデータに変換してストリームに書き込む。
+ isValidJSONObject:
指定されたFondationのidがJSONデータに変換可能か確認する(だと思う)


ようするに、JSONデータとFondationの配列/辞書に相互に変換するメソッド。JSONデータについては、NSDataとストリームの2種類に対応ということだ。




これで、前回のRESTを使ったアプリケーションをJSON対応に変更できるはずだ。ただ、まだ、NSJSONSerializationを使った情報が世の中には少ないのと、railsアプリケーションが返す結果が、著者が想像していたのと事なってので、試行錯誤はあったが。




以下が、試行錯誤したコードだ。




- (IBAction)sendPost:(id)sender
{
    NSURL   *url = [NSURL URLWithString:@"http://localhost:3000/people.json"];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
    [request setHTTPMethod:@"POST"];
    NSMutableDictionary *person = [NSMutableDictionary dictionary];
    [person setValue:@"MURAKAMI Yukio" forKey:@"name"];
    [person setValue:@"18" forKey:@"age"];
    NSError *error = nil;
    NSData  *content = [NSJSONSerialization dataWithJSONObject:person options:NSJSONWritingPrettyPrinted error:&error];
    [request setHTTPBody:content];
    NSHTTPURLResponse   *response = nil;
    NSData  *data = [NSURLConnection sendSynchronousRequest:request
                                          returningResponse:&response
                                                      error:&error];
    content = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&error];
    if (!error) {
        NSString    *s = [[NSString alloc] initWithFormat:@"status code: %d\ndata: %@", [response statusCode], content];
        self.textView.text = s;
        NSLog(@"%@", self.textView.text);
    }
    else {
        NSString    *s = [[NSString alloc] initWithFormat:@"error: %@", error];
        self.textView.text = s;
        NSLog(@"%@", self.textView.text);
    }
}
 
- (IBAction)sendGet:(id)sender
{
    NSURL   *url = [NSURL URLWithString:@"http://localhost:3000/people/1.json"];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    [request setHTTPMethod:@"GET"];
    NSHTTPURLResponse   *response = nil;
    NSError *error = nil;
    NSData  *data = [NSURLConnection sendSynchronousRequest:request
                                          returningResponse:&response
                                                      error:&error];
    NSDictionary    *content = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&error];
    if (!error) {
        NSString    *s = [[NSString alloc] initWithFormat:@"status code: %d\ndata: %@", [response statusCode], content];
        self.textView.text = s;
        NSLog(@"%@", self.textView.text);
    }
    else {
        NSString    *s = [[NSString alloc] initWithFormat:@"error: %@", error];
        self.textView.text = s;
        NSLog(@"%@", self.textView.text);
    }
}
 
- (IBAction)sendGetList:(id)sender
{
    NSURL   *url = [NSURL URLWithString:@"http://localhost:3000/people.json"];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    [request setHTTPMethod:@"GET"];
    NSHTTPURLResponse   *response = nil;
    NSError *error = nil;
    NSData  *data = [NSURLConnection sendSynchronousRequest:request
                                          returningResponse:&response
                                                      error:&error];
    NSDictionary    *content = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&error];
    if (!error) {
        NSString    *s = [[NSString alloc] initWithFormat:@"status code: %d\ndata: %@", [response statusCode], content];
        self.textView.text = s;
        NSLog(@"%@", self.textView.text);
    }
    else {
        NSString    *s = [[NSString alloc] initWithFormat:@"error: %@", error];
        self.textView.text = s;
        NSLog(@"%@", self.textView.text);
    }
}
 
- (IBAction)sendPut:(id)sender
{
    NSURL   *url = [NSURL URLWithString:@"http://localhost:3000/people/1.json"];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
    [request setHTTPMethod:@"PUT"];
    NSMutableDictionary *person = [NSMutableDictionary dictionary];
    [person setValue:@"MURAKAMI Yukio" forKey:@"name"];
    [person setValue:@"81" forKey:@"age"];
    NSError *error = nil;
    NSData  *content = [NSJSONSerialization dataWithJSONObject:person options:NSJSONWritingPrettyPrinted error:&error];
    [request setHTTPBody:content];
    NSHTTPURLResponse   *response = nil;
    NSData  *data = [NSURLConnection sendSynchronousRequest:request
                                          returningResponse:&response
                                                      error:&error];
    content = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&error];
    if (!error) {
        NSString    *s = [[NSString alloc] initWithFormat:@"status code: %d\ndata: %@", [response statusCode], content];
        self.textView.text = s;
        NSLog(@"%@", self.textView.text);
    }
    else {
        NSString    *s = [[NSString alloc] initWithFormat:@"error: %@", error];
        self.textView.text = s;
        NSLog(@"%@", self.textView.text);
    }
}
 
- (IBAction)sendDelete:(id)sender
{
    NSURL   *url = [NSURL URLWithString:@"http://localhost:3000/people/1.json"];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
    [request setHTTPMethod:@"DELETE"];
    NSHTTPURLResponse   *response = nil;
    NSError *error = nil;
    NSData  *data = [NSURLConnection sendSynchronousRequest:request
                                          returningResponse:&response
                                                      error:&error];
    NSDictionary    *content = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&error];
    if (!error) {
        NSString    *s = [[NSString alloc] initWithFormat:@"status code: %d\ndata: %@", [response statusCode], content];
        self.textView.text = s;
        NSLog(@"%@", self.textView.text);
    }
    else {
        NSString    *s = [[NSString alloc] initWithFormat:@"error: %@", error];
        self.textView.text = s;
        NSLog(@"%@", self.textView.text);
    }
}



JSONのContent-Typeは、application/jsonなのですね。RESTの時のapplication/xmlのままにしていたら、railsアプリケーションの方でXMLとして処理をしようとして、エラーになってしまった。


また、成功したら、HTTPステータスは200だと思っていたら、そうではなかった。それは、railsアプリケーションのpeople_controller.rbで以下のように記述しているからだと思う。




  # POST /people
  # POST /people.json
  def create
    @person = Person.new(params[:person])


    respond_to do |format|
      if @person.save
        format.html { redirect_to @person, :notice => 'Person was successfully created.' }
        format.xml { render :xml => @person, :status => :created, :location => @person }
        format.json { render :json => @person, :status => :created, :location => @person }
      else
        format.html { render :action => "new" }
        format.xml { render :xml => @person.errors, :status => :unprocessable_entity }
        format.json { render :json => @person.errors, :status => :unprocessable_entity }
      end
    end
  end



Rubyを知らず、仕様も確認しないで当てずっぽうだが、POSTの場合、:statusを:createdに設定しているのでHTTPステータスは201(created)を返す。




  # PUT /people/1
  # PUT /people/1.json
  def update
    @person = Person.find(params[:id])


    respond_to do |format|
      if @person.update_attributes(params[:person])
        format.html { redirect_to @person, :notice => 'Person was successfully updated.' }
        format.xml { head :no_content }
        format.json { head :no_content }
      else
        format.html { render :action => "edit" }
        format.xml { render :xml => @person.errors, :status => :unprocessable_entity }
        format.json { render :json => @person.errors, :status => :unprocessable_entity }
      end
    end
  end



PUTの場合、:statusを:no_contentに設定しているので、HTTPステータスは204(no content)を返すようだ。何故、エラーとなるのか、少し、驚いてしまった。




ソースコード
GitHubからどうぞ。

https://github.com/murakami/workbook/tree/master/ios/JSON - GitHub


関連情報
JSONの紹介

RFC4627

iOSプログラミング逆引きリファレンス108 ~知りたいことがすぐわかるiPhoneプログラミングテクニック~

NSJSONSerialization Class Reference

Developerサイトの情報。

[iOS][Web]iPhoneアプリケーションとサーバ間の通信(その2)

Ruby on Railsは初めてなので、ログ取りにうってつけな状況だ。Railsアプリケーションを配置するディレクトリを用意する。ディレクトリの場所には制約はないようなので、Documentsディレクトリ配下にrailsというディレクトリを作成して、そこにworkbookというアプリケーション環境を生成する。




$ rails new workbook
      create  
      create  README.rdoc
  ....
Enter your password to install the bundled RubyGems to your system: ←パスワード入力
  ....
Your bundle is complete! Use `bundle show [gemname]` to see where a bundled gem is installed.



workbookアプリケーションのディレクトリ配下に移動して、雛形scaffoldを使って、コードを生成する。




$cd workbook/
$ rails generate scaffold person name:string age:integer
  ....
Could not find coffee-script-source-1.3.1 in any of the sources
Run `bundle install` to install missing gems.



自分の場合はエラーとなったので、素直にコメントの操作を実行して、成功するまで、コード生成を繰り返した。




$ bundle install
$ rails generate scaffold person name:string age:integer
Could not find sass-rails-3.2.5 in any of the sources
Run `bundle install` to install missing gems.
  ....
$ bundle install
$ rails generate scaffold person name:string age:integer
  ....
      invoke  scss
      create    app/assets/stylesheets/scaffolds.css.scss
$ rake db:migrate
==  CreatePeople: migrating ===================================================
-- create_table(:people)
   -> 0.0009s
==  CreatePeople: migrated (0.0010s) ==========================================



初心者の自分では、理由は分からないが、『iOSプログラミング逆引きリファレンス108』で説明されているとおり、設定を変更する。




$ cd app/controllers
$ vi application_controller.rb
  ....
$ cat application_controller.rb
class ApplicationController < ActionController::Base
  #protect_from_forgery
end



protect_from_forgeryをコメント・アウトする。


ところが、著者の場合、これでは上手くいかなかった。今の環境では、この手順だとHTMLとJSONのみの対応で、RESTはXMLということになると思うが、これに対応していない。また、驚いたのはpersonというモデルを作成したが、コントローラ名はこれの複数形のpeopleとなっていた。


RESTに対応される為、people_controller.rbで、JSONの記述をコピーして、JSONの部分をXMLに変更する。




具体的にいうと、例えば、




respond_to do |format|
      format.html # index.html.erb
      format.json { render :json => @people }
    end



となっている部分に、RESTに対応させるため、XMLのコードを追加する。




respond_to do |format|
      format.html # index.html.erb
      format.xml { render :xml => @people }  ←追加する。
      format.json { render :json => @people }
    end



Ruby on Railsやscaffoldが素晴らしいのだと思うが、JSONの記述を真似るだけで対応できた。全てを確認した訳ではないが。




これで、準備OK。サーバ側のアプリケーションを起動する。




$ rails server
=> Booting WEBrick
=> Rails 3.2.3 application starting in development on http://0.0.0.0:3000
=> Call with -d to detach
=> Ctrl-C to shutdown server
[2012-04-29 07:53:27] INFO  WEBrick 1.3.1
[2012-04-29 07:53:27] INFO  ruby 1.8.7 (2010-01-10) [universal-darwin11.0]
[2012-04-29 07:53:27] INFO  WEBrick::HTTPServer#start: pid=1652 port=3000



余談だが、今回の試験で、サーバ側のDBを更新するが、初期状態に戻したくなると思うが、著者は以下の手順で対応した。




$ cd ~/Documents/rails/workbook/db
$ sqlite3 ./development.sqlite3
sqlite> delete from people;  ←全データを削除
sqlite> select * from people;  ←削除された事を確認。
sqlite> update sqlite_sequence set seq=0 where name='people';  ←カウンタをリセット。
sqlite> vacuum;
sqlite> .exit



iPhoneアプリケーション側のコードは、『iOSプログラミング逆引きリファレンス108』そのままなので、詳しくは説明しない。




- (IBAction)sendPost:(id)sender
{
    NSURL   *url = [NSURL URLWithString:@"http://localhost:3000/people.xml"];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    [request setValue:@"application/xml" forHTTPHeaderField:@"Content-Type"];
    [request setHTTPMethod:@"POST"];
    NSString    *content = @"Yukio MURAKAMI17";
    [request setHTTPBody:[content dataUsingEncoding:NSUTF8StringEncoding]];
    NSHTTPURLResponse   *response = nil;
    NSError *error = nil;
    NSData  *data = [NSURLConnection sendSynchronousRequest:request
                                          returningResponse:&response
                                                      error:&error];
    if (!error) {
        NSString    *s = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
        NSString    *t = [[NSString alloc] initWithFormat:@"status code: %d\ndata: %@", [response statusCode], s];
        self.textView.text = t;
        NSLog(@"%@", self.textView.text);
    }
    else {
        NSString    *s = [[NSString alloc] initWithFormat:@"error: %@", error];
        self.textView.text = s;
        NSLog(@"%@", self.textView.text);
    }
}



RESTで気になるといえば、上記の場合、値を渡す際に、XML文字列をコードを埋め込む事だ。せっかくならCocoaの辞書型が使えたらと思うが、その場合は、JSONということになるようだ。JSONは、次回に取り上げる予定だ。




ソースコード
GitHubからどうぞ。

https://github.com/murakami/workbook/tree/master/ios/REST - GitHub


関連情報
Ruby on Rails

プロジェクトページ。

CHAPTER 5 Representational State Transfer (REST)

Fieldingの論文。

iOSプログラミング逆引きリファレンス108 ~知りたいことがすぐわかるiPhoneプログラミングテクニック~

全面的に、この書籍を参考にしています。助かります!

URL Loading System Programming Guide

Developerサイトの情報。