[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];
}
}
実行。
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;
}];
ソースコード
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;
}
これは、実機でないと上手く試せないと思う。
ソースコード
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つだけだ。
ようするに、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 MURAKAMI 17 ";
[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サイトの情報。