iOS/iPhone/iPad/watchOS/tvOS/MacOSX/Android プログラミング, Objective-C, Cocoa, Swiftなど
先日の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で解析する場合、ステータスを覚えておいて、ステータスに合わせて得られた情報を扱う必要があるが、この程度の簡素なデータでは、特に問題はないと思う。
独自に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クラスのインスタンスを得られる。
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を設定する。
以前の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間の通信は、フレームワークのメソッドを通じて行われている為、アプリケーションは、ネットワーク通信を行っている事を意識しない方法になっている。
今回も動作確認はまだで、話のみ。詳しくは、『iPhoneアプリ開発 熟達テクニック(林晃 著)』で確認して欲しい。また、Appleの文書の説明もいいので、こちらでも確認して欲しい。
通知する電文には、単純形式と拡張形式の2種類があるようだが、サンプル・コードは単純形式を利用している。
電文の形式を図にすると以下のとおり。
通知先のデバイスが複数ある場合は、その個数分書き込むことになるが、APNs側にデバイスのリストと電文を渡して、後は宜しくとしてしまうと、APNs側の負担が高くなるので、独自サーバ側で宛先分書き込ませているのか?また、同一セッションで複数個書き込むのが、APNs側にとって、やさしい対応ということになるのだろう。
アプリケーションの通知を受ける側で呼ばれるメソッドは、Local NotificationとPush Notificationで異なる部分があるようだ。
Push Notificationについては、動作確認できる、公開可能なコードが用意できたら、もう少し踏み込んで発表したいと思っている。
前回からの変更点。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 *appl = nil;
appl = (AppDelegate *)[[UIApplication sharedApplication] delegate];
self.document = appl.document;
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;
}
これだと、どんなGPXデータが生成されているのか分からないので、ダンプする機能を追加。
- (IBAction)dump:(id)sender
{
self.gpxTextView.text = self.document.gpxRoot.gpx;
}
これは、実機でないと上手く試せないと思う。
位置情報から住所を取得するのに挑戦だ。
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;
}];
毎度、知らない事を題材にしているので内容に自信はないのだが、今回は、特に自信がない。理解していない部分があるが、前に進む為に挑戦してみる。
今まで著者は、主に通常のビューに対する描画を扱ってきたが、今回はレイヤを使った描画に挑戦だ。
生成した画像やレイヤを管理する為のプロパティを用意した。
@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];
}
}
実行。
ソフトウェア開発の色んな局面で出てくるものとして、ルールか出来ない領域だとは分かるが、ルール化による基準を設けないと、結果の検証が出来ず、次に繋がらない。規模の見積もりも同様だろう。
過去の経験から規模を予想する方法は、人によって結果の差があったり、ちょっとした差異の影響を結果に取り込む事ができないという課題があるが、経験が少ないと、予想できないという問題がある。個人で仕事をやっていて先輩のアドバイスを聞けない場合やスタートアップ時は、これは辛い。
それで、色々調べてみて見つけた方法を紹介する。
例えば、アンケート等で得られた情報から、その国の国民について予測する方法があると思う。それと同じ考えだ。
全体の個数が分かるという考えから、要件から開発する機能をリストアップし、それをある程度の粒度に分割する。
その中から、数個選んでプロトタイプを作成する。そもそも、アプリケーション開発では試してみないと分からない部分があり、事前にプロトタイプを作成すると思うが、それを規模の見積もりに利用するという事だ。
作成したプロトタイプのステップ数を計算する。
次に標本平均と標準偏差を計算する。そして、以下の計算式から、95%と99%の下限・上限の推定値を計算する。
具体的な計算方法については、『SEのための見積りの基本』に計算用のExcelファイルの作り方が説明されているので、それを参考にして欲しい。
実際やって気がついた事は、本当に無作為抽出になっているか?だ。プロトタイプが作りやすい項目が選ばれていないか?プロトタイプもきちんと完成させるところを省略しがちなので、選択する機能の項目には偏りが発生していると思われる。なので、精度を高めるには、機能分割の方法と、プロトタイプを作成する機能が、ある機能の一部というより、それ自体が独立した小さな機能になるように心がけるということかな?と思っている。
Facebook SDK for iOSの情報は豊富にあると思うので、著者が実施した手順について説明する。
豊富な情報があるが、新旧の情報もあり迷うと思うが、著者はFacebook開発者サイトの情報をベースに作業を進めた。
まずは、以下のサイトで制作しているアプリケーションの情報を登録した。ただし、今回はサンプルの為の登録の為、仮の内容なので後でこれが問題になると思う。やってみないとどうなるか分からないので、問題が発生したら考えることにする。
https://developers.facebook.com/apps/
Facebook SDKのソースコードをプロジェクトに追加する方法と、静的ライブラリを作成して、それをプロジェクトに追加する方法があるようだが、ARCを有効にした場合、SDKのソースはARC未対応、ようするにreleaseが残っているのでそれの対応が面倒という事で、静的ライブラリの作成がおすすめだそうだ。
著者は、git closeでSDKを取得したが、SDKを置いているディレクトリで以下のコマンドを実行した。
$ cd ~/facebook/
$ ~/facebook/facebook-ios-sdk/scripts/build_facebook_ios_sdk_static_lib.sh
Project Home: ~/facebook/facebook-ios-sdk
Start Universal facebook-ios-sdk SDK Generation
Step 1 : facebook-ios-sdk SDK Build Library for simulator and device architecture
Build settings from command line:
SDKROOT = iphonesimulator5.0
SYMROOT = ~/facebook/facebook-ios-sdk/build
=== CLEAN NATIVE TARGET facebook-ios-sdk OF PROJECT facebook-ios-sdk WITH CONFIGURATION Release ===
Check dependencies
:
** CLEAN SUCCEEDED **
=== BUILD NATIVE TARGET facebook-ios-sdk OF PROJECT facebook-ios-sdk WITH CONFIGURATION Release ===
Check dependencies
:
** BUILD SUCCEEDED **
Build settings from command line:
SDKROOT = iphoneos5.0
SYMROOT = ~/facebook/facebook-ios-sdk/build
=== CLEAN NATIVE TARGET facebook-ios-sdk OF PROJECT facebook-ios-sdk WITH CONFIGURATION Release ===
Check dependencies
:
** CLEAN SUCCEEDED **
=== BUILD NATIVE TARGET facebook-ios-sdk OF PROJECT facebook-ios-sdk WITH CONFIGURATION Release ===
Check dependencies
:
** BUILD SUCCEEDED **
Step 2 : Remove older SDK Directory
Step 3 : Create new SDK Directory Version
Step 4 : Create combine lib files for various platforms into one
Step 5 : Copy headers Needed
Step 6 : Copy other file needed like bundle
Finished Universal facebook-ios-sdk SDK Generation
You can now use the static library that can be found at:
~/facebook/facebook-ios-sdk/lib/facebook-ios-sdk
Just drag the facebook-ios-sdk directory into your project to include the Facebook iOS SDK static library
2回ビルドしているのは、シミュレータ用と実機用という事のようだ。
SDKのディレクトリに移動してみると、libディレクトリが作成されていて、そこにライブラリのアーカイブとヘッダ・ファイル類が置かれていた。
$ cd facebook-ios-sdk/
$ ls
README.mdown lib scripts test
build sample src
$ ls lib
facebook-ios-sdk
$ ls lib/facebook-ios-sdk/
FBConnect.h NSObject+SBJSON.h
FBDialog.bundle NSString+SBJSON.h
FBDialog.h SBJSON.h
FBFrictionlessRequestSettings.h SBJsonBase.h
FBLoginDialog.h SBJsonParser.h
FBRequest.h SBJsonWriter.h
Facebook.h libfacebook_ios_sdk.a
JSON.h
今回はここまで。続きは次回。
Facebook Developersサイトの説明が素晴らしいのと、アプリケーションとして登録する必要があるため、サンプルとしては困る部分があるので、著者が工夫した事について説明する。
Facebook SDKを使ったアプリケーションをGitHubで公開する場合に気をつけた事に、Facebook SDKのソースをサンプルのgitに含めないことだ。前回作成した静的ライブラリ一式をXcodeのプロジェクトにドラッグ&ドロップすると、それをgitに追加しようとする。
そこで、Finderの操作で静的ライブラリ一式をプロジェクトのフォルダにコピーしておいて、それをXcodeのプロジェクトにドラッグ&ドロップする。
こうすれば、既にディレクトリ上に存在する為、Xcodeはそれをgitに追加しようとしないようだ。
Facebook対応で最初に行うのは、OAuth対応だと思うが、機器上にFacebookアプリがない場合はSafariで、ある場合はFacebookアプリで認証を行うようだ。なので、シミュレータと実機で異なる処理の流れになる。
そして、認証後にアプリケーションに戻ってくる為に、URLスキームを登録するようだ。
URLスキームの頭の"fb"でFacebookであるとの、その後ろにつけるアプリケーションIDでアプリケーションを区別して、認証後に戻るアプリケーションを呼び出す仕組みのようだ。
アプリ内課金は、暫くは使わないだろうと考え後回しにしていたのだが、避ける事が出来なくなったようだ。
Store Kitフレームワークを使ったIn-App Purchaseの何が面倒かというと、環境を用意するところだと思う。
今回はサンプル・コードは用意できていない。なぜなら、登録の都合上、弊社の開発中のアプリケーションとしてアプリケーションを制作したからだ。申し訳ない。
iOS Provisioning Portalで、App IDを登録する。
iTunes ConnectのManage Your Applicationsで、アプリケーションを追加する。
Manage In-App Purchasesでプロダクトを追加する。
XcodeのiOSアプリケーションのプロジェクトを生成して、StoreKit.frameworkを追加し、StoreKit/StoreKit.hをimportする。
SKRequestDelegateとSKProductsRequestDelegateの2つのプロトコルに対応させる。
#import <UIKit/UIKit.h>
#import <StoreKit/StoreKit.h>
@interface ViewController : UIViewController <SKRequestDelegate, SKProductsRequestDelegate>
@end
アプリ内課金に対応しているか確認し、指定したプロダクトIDの情報を取得する。
- (void)viewDidLoad
{
[super viewDidLoad];
if ([SKPaymentQueue canMakePayments]) {
NSLog(@"ユーザにStoreを表示する");
[self requestProductData];
}
else {
NSLog(@"購入できないことをユーザに警告する");
}
}
プロダクトIDの情報取得を要求し、結果を受け取るメソッドは以下のとおり。
- (void)requestProductData
{
SKProductsRequest *request= [[SKProductsRequest alloc]
initWithProductIdentifiers:
[NSSet setWithObject:<プロダクトID>]];
request.delegate = self;
[request start];
}
- (void)productsRequest:(SKProductsRequest *)request
didReceiveResponse:(SKProductsResponse *)response
{
NSArray *myProduct = response.products;
NSLog(@"%@", myProduct);
SKProduct *skp = [myProduct objectAtIndex:0U];
NSLog(@"%@", skp.localizedDescription);
}
準備は面倒だが、この程度なら、あっけなく動いた。
記録によると第1回が開催されたのが2003年10月4日なので、今年で9年目。その間、Cocoaを巡る周りの状況は大きく変わったものだ。感慨深い。
発表の内容をざっと紹介すると、iCabで使用されているAppLink、見積もりのお話、クラウド化する開発者の作業環境のディスカッション。そして、今回はディスカッションの時間を長めにとって、色々な話題が議論された。7月からアプリケーション開発を主な業務にして行く予定なので、とても参考にある貴重な情報が聞けて、有意義だった。
前回は、登録したプロダクトの情報が取得されたまでで、今回はそれを選択して、購入してみる。
開発者サイトの情報には制限があるので、概要の説明で我慢して欲しい。
著者の経験から最初にやっておいた方がいいのが、アプリケーション内課金用の試験アカウントを用意だ。試験用とはいえ、通常のApple IDと同様な条件で作成なので、色々と準備しておく事があるので、デバッグ段階だと慌ててしまうと思う。やり方だが、iTunes ConnectのManag UsersでTest Userを作成する。操作自体は、それ程、難しいものではないと思う。
購入処理を行うクラスに、SKPaymentTransactionObserverを設定する。
#import <UIKit/UIKit.h>
#import <StoreKit/StoreKit.h>
@interface ViewController : UIViewController
<SKRequestDelegate, SKProductsRequestDelegate, SKPaymentTransactionObserver>
@end
初期化を行う箇所に以下のコードを追加する。サンプルでは、SKPaymentTransactionObserverプロトコルを設定した、ビュー・コントローラを指定している。
- (void)viewDidLoad
{
[super viewDidLoad];
:
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
}
購入やリストアのトランザクションに対応するメソッドを実装する。
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
{
for (SKPaymentTransaction *transaction in transactions)
{
switch (transaction.transactionState)
{
case SKPaymentTransactionStatePurchased:
NSLog(@"%s, SKPaymentTransactionStatePurchased", __func__);
[self completeTransaction:transaction];
break;
case SKPaymentTransactionStateFailed:
NSLog(@"%s, SKPaymentTransactionStateFailed", __func__);
[self failedTransaction:transaction];
break;
case SKPaymentTransactionStateRestored:
NSLog(@"%s, SKPaymentTransactionStateRestored", __func__);
[self restoreTransaction:transaction];
default:
break;
}
}
}
- (void)completeTransaction:(SKPaymentTransaction *)transaction
{
//[self recordTransaction: transaction];
//[self provideContent: transaction.payment.productIdentifier];
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
- (void)failedTransaction:(SKPaymentTransaction *)transaction
{
if (transaction.error.code != SKErrorPaymentCancelled) {
NSLog(@"transaction.error.code is not SKErrorPaymentCancelled");
}
[[SKPaymentQueue defaultQueue] finishTransaction: transaction];
}
- (void)restoreTransaction:(SKPaymentTransaction *)transaction
{
//[self recordTransaction: transaction];
//[self provideContent:transaction.originalTransaction.payment.productIdentifier];
[[SKPaymentQueue defaultQueue] finishTransaction: transaction];
}
本当に仮の実装。前回のプロダクト情報を取得するメソッドで、取得したプロダクトを購入する。
- (void)productsRequest:(SKProductsRequest *)request
didReceiveResponse:(SKProductsResponse *)response
{
for (SKProduct *product in response.products) {
SKPayment *payment = [SKPayment paymentWithProduct:product];
[[SKPaymentQueue defaultQueue] addPayment:payment];
}
}
シミュレータで実行。すると、本当に購入するか聞いてきた。
購入を選択すると、シミュレータなのでIDを聞いてきた。先ほど作成した試験用IDを指定すればいい。
すると、完了が通知された。
デバッグ出力も印字されている。
-[ViewController paymentQueue:updatedTransactions:], SKPaymentTransactionStatePurchased
アプリケーションは購入情報を得ても、外部のサーバが購入情報によって何らかの対応を行う場合は、それが正しい物である事を確認できないといけない。ということで、Storeレシートの確認に挑戦する。
外部サーバとの通信が必要ない場合。例えば、購入されると、アプリケーション内の機能制限フラグを落として、隠し機能を有効にする場合は、前回までの方法で対応できる。サーバから、購入に対応するデータを取得して、アプリケーションで利用できるようにする場合、外部のサーバへの通知が本当に正しいのか気にする必要があるようだ。そこで、購入が成功するとStoreKitからレシートを渡されるので、されを外部サーバに渡し、外部サーバがSoteにレシートの内容が正しい事を確認するという仕組みのようだ。
サンプルでは、購入成功を受けたメソッドで、簡易的にアプリケーションからStoreにレシートの内容を確認している。
まず初めに、サンプル・コードはBASE64にエンコードするコードだ。簡易なもので、テストも不十分なのであしからず。
#define BASE64PAD @"="
static const char base64Alphabet[64] = {
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
'w', 'x', 'y', 'z', '0', '1', '2', '3',
'4', '5', '6', '7', '8', '9', '+', '/'
};
- (NSString *)stringEncodedWithBase64:(NSData *)data
{
if (! data) return nil;
NSUInteger dataLen = data.length;
unsigned char *dataBytes = (unsigned char *)[data bytes];
NSMutableString *str = [[NSMutableString alloc] init];
NSUInteger dataIndex = 0;
while (dataIndex < dataLen) {
char d[3] = {0, 0, 0};
d[0] = dataBytes[dataIndex];
if ((dataIndex + 1) < dataLen)
d[1] = dataBytes[dataIndex + 1];
if ((dataIndex + 2) < dataLen)
d[2] = dataBytes[dataIndex + 2];
NSUInteger bit6 = 0;
char s[5];
bit6 = (d[0] >> 2) & 0x3F;
s[0] = base64Alphabet[bit6];
bit6 = ((d[1] >> 4) & 0x0F) | ((d[0] << 4) & 0x3F);
s[1] = base64Alphabet[bit6];
bit6 = ((d[2] >> 6) & 0x03) | ((d[1] << 2) & 0x3F);
s[2] = base64Alphabet[bit6];
bit6 = d[2] & 0x3F;
s[3] = base64Alphabet[bit6];
s[4] = '\0';
[str appendString:[NSString stringWithCString:s encoding:NSASCIIStringEncoding]];
dataIndex += 3;
}
if (dataIndex < dataLen) {
NSRange aRange = NSMakeRange(dataLen + 2, (dataIndex - dataLen));
[str replaceCharactersInRange:(NSRange)aRange withString:BASE64PAD];
}
NSUInteger padNum = [str length] % 4;
for (NSUInteger i = 0; i < padNum; i++) {
[str appendString:BASE64PAD];
}
return str;
}
自分自身が分かりやすい事を優先したコードなので、ちょっと、恥ずかしい。
次はレシートを確認するコードだ。
- (void)completeTransaction:(SKPaymentTransaction *)transaction
{
/* NSURL *url = [NSURL URLWithString:@"https://buy.itunes.apple.com/verifyReceipt"]; */
NSURL *url = [NSURL URLWithString:@"https://sandbox.itunes.apple.com/verifyReceipt"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[request setHTTPMethod:@"POST"];
NSString *json = [NSString stringWithFormat:@"{\"receipt-data\" :\"%@\"}",
[self stringEncodedWithBase64:transaction.transactionReceipt]];
[request setHTTPBody:[json dataUsingEncoding:NSUTF8StringEncoding]];
NSURLResponse *response;
NSError *error;
NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
NSLog(@"%@", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
通信先のURLは"https://buy.itunes.apple.com/verifyReceipt"が正しいのだが、テストなのでサンドボックス環境の"https://sandbox.itunes.apple.com/verifyReceipt"を設定している。
transaction.transactionReceiptがレシートで、これを外部のサーバに私て、外部サーバが上記のようなコードを例えば、PythonやRuby、PHPで記述して、レシートを確認するという事だ。
以下が結果。
{
"receipt":{
...
"status":0}
正しいということか。
UIAlertViewについては、もっとカスタマイズできるようにして欲しいが、何故かあまり手をつけられていない。Blocks対応もまだなので、もしかしたら、何か大きな変更を考えているのかもしれない。
アラートで画像等を表示したくなった。そも最も簡単な方法が画像のビューを追加する方法だが、追加するだけだとアラート全体のサイズが変更されないので、追加した画像と、元々のボタンが重なる等、不具合が発生する。そこで、自分が追加した画像のサイズから、アラート全体とボタンの位置を調整する事になる。
- (IBAction)alertImage:(id)sender
{
/* 画像を用意 */
UIImage *image = [UIImage imageNamed:@"likeness.png"];
UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
imageView.frame = CGRectMake(10.0, 80.0, 100.0, 100.0);
UIAlertView *alertView = [[UIAlertView alloc] init];
alertView.delegate = self;
alertView.title = @"Alert Image";
alertView.message = @"a likeness";
[alertView addSubview:imageView]; /* 画像を追加 */
[alertView addButtonWithTitle:@"NO"];
[alertView addButtonWithTitle:@"YES"];
alertView.cancelButtonIndex = 0;
[alertView show];
}
/* アラートのボタン押下に対応 */
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
DBGMSG(@"%s, buttonIndex(%d)", __func__, (int)buttonIndex);
}
/* アラートのサイズを調整する */
- (void)willPresentAlertView:(UIAlertView *)alertView
{
CGRect frame = alertView.frame;
frame.origin.y -= 50.0; /* 追加した分、縦軸の座標を調整 */
frame.size.height += 100.0; /* 追加した分、縦の長さを増やす */
alertView.frame = frame;
for (UIView* view in alertView.subviews) {
frame = view.frame;
if (frame.origin.y > 80) { /* 追加したビューより下 */
frame.origin.y += 100; /* 追加した分、縦軸の座標を調整 */
view.frame = frame;
}
}
}
上手く表示されたが、何度もテストして位置の調整が必要そうで、あまり、お勧めできるもので内容に思えるので、次回は違う方法を模索してみたい。
アラートの内容をカスタマイズするなら、ビューコントローラにしてnibにしてしまえば?ということで、やってみました。
アラート風のビューコントローラを呼び出すコードだ。
- (IBAction)modalPane:(id)sender
{
ModalPaneViewController *modalPaneViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"ModalPaneViewController"];
[modalPaneViewController setCompletionHandler:^(ModalPaneViewControllerResult result) {
switch (result) {
case ModalPaneViewControllerResultCancelled:
[self performSelectorOnMainThread:@selector(didCancel:) withObject:nil waitUntilDone:NO];
break;
case ModalPaneViewControllerResultDone:
[self performSelectorOnMainThread:@selector(didDone:) withObject:nil waitUntilDone:NO];
break;
default:
break;
}
[self dismissModalViewControllerAnimated:YES];
}];
[self presentModalViewController:modalPaneViewController animated:YES];
}
- (void)didDone:(id)arg
{
DBGMSG(@"%s", __func__);
}
- (void)didCancel:(id)arg
{
DBGMSG(@"%s", __func__);
}
アラート風ビューコントローラのコードは、以前紹介したModelPaneとなる。
typedef enum ModalPaneViewControllerResult {
ModalPaneViewControllerResultCancelled,
ModalPaneViewControllerResultDone
} ModalPaneViewControllerResult;
typedef void (^ModalPaneViewControllerCompletionHandler)(ModalPaneViewControllerResult result);
@interface ModalPaneViewController : UIViewController
@property (nonatomic, copy) ModalPaneViewControllerCompletionHandler completionHandler;
- (IBAction)done:(id)sender;
- (IBAction)cancel:(id)sender;
@end
@implementation ModalPaneViewController
@synthesize completionHandler = _completionHandler;
- (void)viewDidLoad
{
[super viewDidLoad];
}
- (void)viewDidUnload
{
self.delegate = nil;
self.completionHandler = nil;
[super viewDidUnload];
}
- (IBAction)done:(id)sender
{
DBGMSG(@"%s", __func__);
if (self.completionHandler) {
self.completionHandler(ModalPaneViewControllerResultDone);
}
}
- (IBAction)cancel:(id)sender
{
DBGMSG(@"%s", __func__);
if (self.completionHandler) {
self.completionHandler(ModalPaneViewControllerResultCancelled);
}
}
@end
ただし、ビューコントローラは全画面?なので、背景のビューを透明に設定してみたのだが、上手くいかなかった?何故だ!
何故、透明にならないのか?自動で背景ビューが挿入されるのか?と考えて、以下のメソッドでビュー構造をダンプしてみた。
- (void)dumpView:(id)aView level:(int)level
{
for (int i = 0; i < level; i++) printf("\t");
printf("%s\n", [[NSString stringWithFormat:@"%@", [[aView class] description]] UTF8String]);
for (int i = 0; i < level; i++) printf("\t");
printf("%s\n", [[NSString stringWithFormat:@"%@", NSStringFromCGRect([aView frame])] UTF8String]);
for (UIView *subview in [aView subviews]) {
[self dumpView:subview level:(level + 1)];
}
}
結果は以下のとおり。
UIView
{{0, 20}, {320, 460}}
UIView
{{60, 130}, {200, 200}}
UIRoundedRectButton
{{20, 143}, {72, 37}}
UIButtonLabel
{{24, 9}, {23, 19}}
UIRoundedRectButton
{{108, 143}, {72, 37}}
UIButtonLabel
{{20, 9}, {31, 19}}
自動で背景ビューが挿入されている訳ではないようだ。
Storyboardは画面の遷移を扱うので、これでViewControllerを生成すると全画面になる。なので、以前のようにnib(xib)で全画面でないViewControllerを用意すれば、アラート風のモーダル・ビュー・コントローラが出来るのでは?
実行!
駄目だ!モーダル・ビュー・コントローラではなくて、アラート風ビューを子ビューとして追加するしかないみたい。