「第2回 iphone_dev_jp 東京iPhone/Mac勉強会」で発表してきました
こんにちは、k_kinukawaです。
少し前の話になってしまうのですが、第2回 iphone_dev_jp 東京iPhone/Mac勉強会で「mixiのiOSアプリ開発」という題で発表をしてきました。
続きを読むiOS下位互換のための動的メソッド追加
どうも、佐野です。先日のWWDCでは大々的にiOS6が発表され、AppleのDev Centerからβ版のSDKもダウンロードできるようになっており、開発者の皆さんは新機能の利用に胸を踊らせていることかと思います。mixiのiPhoneアプリはiOS4ユーザも多いため旧環境もサポートしなければならず、一方で新環境のユーザには新機能を提供したいですしコードも UP TO DATE に保っておきたいものです。そこで今回はできるだけ新環境向けのコードに下位互換性を持たせられるように、クラスに対して動的にメソッドを追加する方法をご紹介します。
Objective-Cのメソッドコールは、Cのようにコンパイル時にリンクされるのではなく、アプリケーションの実行時にメソッドが検索されて実行されます。実はObjective-Cのメソッドは単なるC言語の関数ポインタであり、それを呼び出すためのセレクタも単なるC言語の文字列なのです。クラスはセレクタに対するメソッドのマップを持っていて、コール時に検索してインスタンスの実態としてのselfを渡して実行しているのです。Objective-Cはこのマップを操作するためのランタイムAPIを提供しています。
さて、例としてiOS5から追加されたUIViewControllerクラスのインスタンスメソッド:
- (void)presentViewController:(UIViewController *)viewControllerToPresent animated:(BOOL)flag completion:(void (^)(void))completion
を見てみましょう。これはiOS2以降で使える:
- (void)presentModalViewController:(UIViewController *)modalViewController animated:(BOOL)animated
と似ていますが、モーダルの表示が完了した時点でブロックを呼び出せる点が異なっており、またiOS5以降ではこのメソッドは非推奨となっています。さて、以下のようにあるボタンを押すと赤い画面がモーダルで表示されるプログラムを考えます:
- (void)presentModalButtonTapped:(id)sender { UIViewController *vc = [[[UIViewController alloc] initWithNibName:nil bundle:nil] autorelease]; vc.view.backgroundColor = [UIColor redColor]; [self presentViewController:vc animated:YES completion:^{ NSLog(@"complete"); }]; }
これをBaseSDKをiOS5.1にしてビルドし、iPhone5.1 Simulatorで実行してみましょう。ちゃんと赤い画面が表示され、コンソールに"complete"と出力されます。しかしDeployment Targetを4.0にしてiPhone4.3 Simulatorで実行すると、以下のようなエラーが表示されアプリがクラッシュします:
2012-06-27 11:48:58.303 Sample[11021:b903] -[SViewController presentViewController:animated:completion:]: unrecognized selector sent to instance 0x4e098e0
iOS4では presentViewController:animated:completion: が実装されていないからですね。でもBase SDKはiOS5.1に設定してあるのでコード中に警告などは表示されません(そもそもXcodeではBase SDKは最新のものしか選べない)。だからiOS5ベースで開発を進めていて、テスト段階になってiOS4環境で実行してエラーが発覚してまた作り直しなんていうことはよく起きます。
こういう場合には presentModalViewController:animated: のみを使うようにしてiOS4でも動くコードに書き直すか、コード内でOSのバージョンによって処理を分岐させるか、はたまた presentViewController:animated:completion: をオーバーライドして同じ機能を自分で作ることもできますが、こういうことばかりしているとコードが下位互換だらけになってきますし、せっかくの新機能が使えなくて悲しい気持ちになってきます。そこで、iOS4で実行された場合のみ自分で定義した代替メソッドが呼ばれるようにランタイムAPIを使ってクラスを操作してみましょう。
まず UIViewController+iOS4Compatible という名前で UIViewController の拡張カテゴリを作り、<objc/runtime.h> をインポートして次のようなメソッドを定義します:
#import <objc/runtime.h> @implementation UIViewController (iOS4Compatible) - (void)iOS4_presentViewController:(UIViewController *)viewControllerToPresent animated:(BOOL)flag completion:(void (^)(void))completion { NSLog(@"called iOS4_presentViewController:animated:completion:"); [self presentModalViewController:viewControllerToPresent animated:flag]; [self performSelector:@selector(callBlock:) withObject:completion afterDelay:(flag) ? 0.5 : 0]; } - (void)callBlock:(void (^)(void))block { if(block) block(); } ...
これは iOS4 で動作する自作の presentViewController:animated:completion: もどきです。presentModalViewController:animated: を使ってモーダルを表示し、その0.5秒後にcompletionブロックを実行するメソッドを呼び出すようになっています(モーダル表示のアニメーションは0.4秒)。続けて iOS4 環境で presentViewController:animated:completion: を呼んだ場合にこの自作メソッドが呼ばれるようにするクラスメソッドを定義します:
... + (void)iOS4compatibilize { Method m1 = class_getInstanceMethod(self.class, @selector(iOS4_presentViewController:animated:completion:)); class_addMethod(self.class, @selector(presentViewController:animated:completion:), method_getImplementation(m1), method_getTypeEncoding(m1)); } @end
class_getInstanceMethod, class_addMethod, method_getImplementation, method_getTypeEncoding という見慣れない4つの関数が出てきました。これは runtime.h で宣言されている関数で、これらによって presentViewController:animated:completion: というセレクタに対して iOS4_presentViewController:animated:completion: というセレクタが対応するメソッドを関連づけることができるのです。
さて、この拡張カテゴリをインポートして、アプリケーションの実行後に:
[UIViewController iOS4compatibilize];
が呼ばれるようにソースコードを修正し、再び iPhone4.3 Simulatorで実行してみましょう。すると iOS5 の場合と全く同じように動作するではありませんか!そしてコンソールに "called iOS4_presentViewController:animated:completion:" と出力されることから、ちゃんと自分で定義したメソッドが呼ばれていることが確認できます。
ここで再びiPhone5.1 Simulatorで実行してみましょう。するとまた同じ動作が見られますが、上の "called iOS4_..." は出力されません。これは class_addMethod が、メソッドが既に定義されている場合には追加をしない仕様になっているためです。これで見事に presentViewController:animated:completion: がiOS4互換となりました!
このやり方の良いところは、プログラム内で下位互換サポートが拡張カテゴリとしてコードから分離できることと、新環境においては無駄なオーバーヘッドなく正式なメソッドを呼び出すことができる点です。Objective-Cではクラスも動的に追加できるので、同様のやり方で新環境で追加されたクラスの呼び出しにも対応することもできるかもしれません。
Objective-Cのランタイムの仕組みはこちらのコラムで詳しく解説されています。2005年の記事なので当時はiPhoneもまだ世に出ていない頃なのが趣き深いですね。それではまたお会いしましょう。
WWDC の楽しみ方 - WWDC 2012 参加報告 -
こんにちわ、iPhone, iPad アプリを開発しております田村です。
最近のマイブームは新タマネギです。
6/11- 6/15 にサンフランシスコで行われた WWDC 2012 に、同チームの k_kinukawa と初参戦してきました。
ここでは、英語が堪能ではない iOS app 開発者の私が、WWDC を 200 % で楽しむために大事だと感じたこと記しておきます。
Blocksを使ったHTTPリクエスト
聖闘士星矢Ωが、思ったより面白くて小宇宙が軽く爆発しそうなk_kinukawaです。
今回は、iOSアプリでHTTP通信を行うときの話です。
2012年4月27日 「メインスレッド上で処理している」について一部修正
従来のNSURLConnectionは、レスポンスをdelegateでハンドリングしていました。
そのため、リクエストを投げる箇所とレスポンスを受ける箇所がコード上で離れてしまい、可読性がよくありませんでした。
また、レスポンスを受け取ったあとの処理についても、delegate内で条件分けをして処理をしているうちに分岐/ネスト地獄になりがちでした。
一方、iOS5からNSURLConnectionにsendAsynchronousRequest:queue:completionHandler:というメソッドが誕生しました。
引数を見る限り、GCDを使って非同期リクエストをする系のメソッドのように見えます。
こいつは便利そうですね!!
しかし、利用可能なのはiOS5以降。iOS4をサポートしている限りこれをそのまま使うことは出来ません><
少し前なら「ネットワーク周りめんどくさい!ASIHTTPRequest使えばいいじゃん!」という流れだったのですが、残念ながらサポート終了されるみたいです。
(↑URLがcool)
mixiでは、iOS4でもBlocksを使ってHTTPレスポンスを非同期にハンドリングできるMixiAsyncURLConnectionというクラスを作り、実際に公式クライアントアプリ内で利用しています。
今回は、MixiAsyncURLConnectionの簡易版(でも必要機能はそろっている)をサンプルコードの形にまとめました。
使い方
#import "MixiAsyncURLConnection.h"
//1.リクエスト作成
NSURLRequest *req = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://mixi.jp/"]];
//2.MixiAsyncURLConnectionオブジェクト作成
MixiAsyncURLConnection * connection = [[[MixiAsyncURLConnection alloc]initWithRequest:req timeoutSec:10.0f completeBlock:^(id connection, NSData *data){
//4.レスポンスハンドリング
self.textView.text = [[[NSString alloc]initWithData:data encoding:NSJapaneseEUCStringEncoding]autorelease];
} progressBlock:nil errorBlock:nil] autorelease];
//3.リクエスト実行
[connection performRequest];
簡単ですね。
順に説明します。
1.リクエスト作成
普通にNSURLRequestを作成します。
NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://localhost:3000/"]];
[req setHTTPMethod:@"POST"];
[req setHTTPBody:[@"data=hogefuga" dataUsingEncoding:NSUTF8StringEncoding]];
のようにすれば、GETだけでなくPOSTリクエストも作成できます。
2.MixiAsyncURLConnectionオブジェクト作成
MixiAsyncURLConnectionクラスのイニシャライザでは、以下のパラメータを指定できます。
- Request – NSURLRequestオブジェクトです
- timeoutSec – タイムアウト時間を指定します
- completeBlock – レスポンス受信完了時に実行されるBlockを指定します
- progressBlock – 途中経過をハンドリングするBlockを指定します
- errorBlock – エラー発生時に実行されるBlockを指定します
3.リクエスト実行
生成されたオブジェクトのperformRequestメソッドを実行すると、リクエストが飛びます。
4.レスポンスハンドリング
レスポンス受信完了すると、非同期でBlockが呼び出されます。
このBlockの中で、受信データをハンドリングします。
もしリクエストキャンセルをしたい場合は、MixiAsyncURLConnectionオブジェクトのcancelメソッドを呼びます。
とても簡単ですね。
特徴
いくつか注目ポイントがあります。
タイムアウト
NSURLRequestのrequestWithURL:cachePolicy:timeoutInterval:を使うと、リクエストのタイムアウトを指定できますが、実は落とし穴があります。
POSTリクエストをする時に、NSMutableURLRequestのsetHTTPMethodメソッドを使うと、タイムアウトがなぜか効かなくなってしまいます。
色々調べた結果、このような情報を見つけました。残念ですね。
そこで、MixiAsyncURLConnectionでは自前のタイムアウト処理を入れてあります。
これで、POSTリクエストでも自由にタイムアウト時間を設定できます。
Blocksによる非同期処理
completeBlock、progressBlock、errorBlockによって、様々な状態をハンドリングできます。
progressBlockを使えば、写真アップロードの途中経過をプログレスバーにリアルタイムで表示するような処理が簡単に書けます。
errorBlockではNSErrorオブジェクトが返ってくるので、リクエストごとにエラーハンドリングを記述することができます。
注意点としては、errorBlockにはiPhoneの通信エラーのようなNSURLConnectionのエラーが返ってきます。
HTTPのレスポンスコード400番台や500番台はcompleteBlockで受け取ります。
メインスレッド上で処理している
GCDとかNSOperationを使った並列ネットワーク処理のサンプルは、調べるといくつも発見することができます。
しかし、WWDCの動画等を見ていると、UIの処理はもちろん、ネットワーク処理もメインスレッド上で行うべきだとの意見もあります。
上の表記についてご指摘を頂きました。下に詳細を追記します。
MixiAsyncURLConnectionでは、GCDもNSOperationも利用していません。
もしレスポンスのハンドリングが重い処理になる時には、completeBlockの中でGCDを使ってタスクキューへ処理を投げてしまうと良さ気です。
追記
WWDCの動画では、「ネットワーク処理もメインスレッド上で行うべき」とは言っていませんでした。
該当する動画はこちらです。
iOS developerのアカウントでログインすると視聴できます。
WWDC2010 video
Network Apps for iPhone OS, Part 2また、ご指摘くださったniwさんに素晴らしい調査レポートブログ記事を紹介していただきました。
とても勉強になります。ありがとうございました。
終わりに
実はMixiAsyncURLConnectionの中身は、たった100行ちょっとのNSURLConnectionクラスの拡張です。
しかし、たったこれだけで非常に使いやすいクラスに化けてくれます。
ひと通り動作確認、メモリリークチェックはしています。
機能も最小限なので、ここから自分好みに拡張していくのも楽しいのではないでしょうか。
UIAlertView の delegate 束縛を解放しよう
どうも、佐野です。今回は UIAlertView
の拡張カテゴリを作って、delegate
ではなく Block
でコールバック処理を記述するための拡張カテゴリの作り方を紹介します。
iOS4 から Objective-C では Blocks という独自のクロージャ機能が搭載されました。これによってアニメーションの記述やコールバック処理などグッと直観的・効率的に記述できるようになったのですが、残念ながら UIKit によって提供されているクラスの多くはまだ Blocks に最適化された作りになっていません。UIAlertView
もそのひとつです。
UIAlertView
は、その delegate
を実装することでユーザアクションに対する処理を記述する訳ですが、どうも使い勝手が悪い。例えば、同じで画面内で2通りの UIAlertView
を表示するような UIViewController
を作る場合、その実装はこんな風になるでしょう:
@implementation MyViewController ... // 1個目のアラートの表示処理 - (IBAction)action1:(id)sender { // アラート生成 UIAlertView *a1 = [[[UIAlertView alloc] initWithTitle:@"Alert1" message:@"..." delegate:self cancelButtonTitle:@"キャンセル" otherButtonTitles:@"OK", nil] autorelease]; // 識別のためのタグを設定 a1.tag = 1; // 表示 [a1 show]; } // 2個目のアラートの表示処理 - (IBAction)action2:(id)sender { // アラート生成 UIAlertView *a2 = [[[UIAlertView alloc] initWithTitle:@"Alert2" message:@"..." delegate:self cancelButtonTitle:@"キャンセル" otherButtonTitles:@"OK", nil] autorelease]; // 識別のためのタグを設定 a2.tag = 2; // 表示 [a2 show]; } ... // アラートのdelegateメソッド // alertViewのtag属性によってアラート1,2を識別し、処理を分岐する - (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex { // 1個目のアラートに対する処理 if(alertView.tag == 1) { if(buttonIndex == 0) { ... // [キャンセル]処理 } else { ... // [OK]処理 } } // 2個目のアラートに対する処理 else if(alertView.tag == 2) { .... } }
別々の UIAlertView
をわざわざ共通の delegate
メソッドに通してその中で再び分岐させているのが嫌です。これが500行ぐらいのコードになると処理の流れが分散してとても読みづらいものになります。ここはやっぱり Blocks を使って、こんな風にカッコ良く書きたいモノです:
// コールバックブロックを生成 void (^alertCallback1)(NSUInteger) = ^(NSUInteger buttonIndex) { if(buttonIndex == 0) { ... // [キャンセル]処理 } else { ... // [OK]処理 } }; // コールバックを渡してアラートを生成 UIAlertView *a1 = [[[UIAlertView alloc] initWithTitle:@"Alert1" message:@"..." callback: alertCallback1 // ← コールバックブロックを引数に! cancelButtonTitle:@"キャンセル" otherButtonTitles:@"OK", nil] autorelease]; // 表示 [a1 show];
alertCallback1
が delegate
メソッドに代わるブロックになっています。あるいは何の変数代入もせずこんな風に書くのも良いでしょう:
// アラート表示 [[[[UIAlertView alloc] initWithTitle:@"Alert1" message:@"..." callback:^(NSInteger buttonIndex) { if(buttonIndex == 0) { ... // [キャンセル]処理 } else { ... // [OK]処理 } } cancelButtonTitle:@"キャンセル" otherButtonTitles:@"OK", nil] autorelease] show];
アラートの生成とその後の処理を同じところにまとめて書けるのがいいですし、UIViewController
が UIAlertViewDelegate
プロトコルを実装する必要もなくなりとてもスッキリします。 という訳で早速この UIAlertView
拡張カテゴリを作りましょう。まずはヘッダ:
UIAlertView+BlocksExtension.h
@interface UIAlertView(BlocksExtension) typedef void (^UIAlertViewCallback_t)(NSInteger buttonIndex); - (id)initWithTitle:(NSString *)title message:(NSString *)message callback:(UIAlertViewCallback_t)callback cancelButtonTitle:(NSString *)cancelButtonTitle otherButtonTitles:(NSString *)otherButtonTitles, ...; @end
(void (^)(NSInteger buttonIndex))
というブロック型を UIAlertViewCallback_t
と typedef
しました。これがクリックされたボタンのインデックスを引数に取るコールバックブロックの型になります。
さて、実装はどうしましょう。このコールバックを呼ぶための delegate
インスタンスを作って UIAlertView
に持たせたい訳ですが、そのメモリ管理をどう行うかが問題になります。UIAlertView
の delegate
プロパティは assign
属性なので、インスタンスによって保持・解放されません。んーむ、どうしよう。
解決!メモリ管理は誰かにやってもらうという発想を捨て、ここでは delegate
インスタンスが、役割を終えたら自ら消滅してくれるようにします。そのクラスを UIAlertViewCallback
として、まずヘッダはこんな感じ:
UIAlertViewCallback.h
@interface UIAlertViewCallback : NSObject <UIAlertViewDelegate> { UIAlertViewCallback_t callback; } @property (nonatomic, copy) UIAlertViewCallback_t callback; - (id)initWithCallback:(UIAlertViewCallback_t) callback; @end
UIAlertViewDelegate プロトコルの実装を宣言している点に注意してください。実装はこうです:
UIAlertViewCallback.m
@implementation UIAlertViewCallback @synthesize callback; - (id)initWithCallback:(UIAlertViewCallback_t)aCallback { if(self = [super init]) { // コールバックブロックをセット self.callback = aCallback; // 自分自身を保持! [self retain]; } return self; } // UIAlertView の delegate メソッド - (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex { // コールバックを呼ぶ if(callback) callback(buttonIndex); // コールバックを呼び終えたら自分自身を解放する! [self release]; } - (void)dealloc { self.callback = nil; [super dealloc]; } @end
まずコンストラクタの中で自分自身を保持しています。これによって他のインスタンスからの参照がなくても dealloc
されることはありません。次に UIAlertViewDelegate
の - (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
を実装しており、その中でコールバックブロックを呼んで、それが終わったら自分自身を解放しています。こうすることで、インスタンスはちゃんと自分の役割を終えたら消滅してくれる訳です!
これでようやく準備完了、あとはこれを使って UIAlertView
の拡張カテゴリを実装しましょう:
UIAlertView+BlocksExtension.m
@implementation UIAlertView(BlocksExtension) - (id)initWithTitle:(NSString *)title message:(NSString *)message callback:(UIAlertViewCallback_t)callback cancelButtonTitle:(NSString *)cancelButtonTitle otherButtonTitles:(NSString *)otherButtonTitles, ... { self = [self initWithTitle:title message:message delegate:nil cancelButtonTitle:cancelButtonTitle otherButtonTitles:nil]; if(self) { // otherButtonTitles, ... を手動でセット va_list args; va_start(args, otherButtonTitles); for (NSString *arg = otherButtonTitles; arg != nil; arg = va_arg(args, NSString*)) { [self addButtonWithTitle:arg]; } va_end(args); // delegateにUIAlertViewCallbackをセット self.delegate = [[[UIAlertViewCallback alloc] initWithCallback:callback] autorelease]; } return self; } @end
va_list
なんかを使っているのは、可変長引数列をそのまま他のメソッドコールの引数として渡すことができないためです。今回のポイントはその後の self.delegate = [[[UIAlertViewCallback alloc] initWithCallback:callback] autorelease];
と、先ほどのコールバック用のインスタンスを生成してセットしているところです。
どうですか、なかなかクールでしょう。このやり方は UIActionSheet
にもそのまま適用できますし、あとは NSURLConnection
をラッピングして通信コールバックをブロックで記述するようなクラスもこのやり方で作れます。条件は Delegator (UIAlertView
, UIActionSheet
, NSURLConnection
など) が消滅するタイミングで、1度だけ必ずコールされる delegate
メソッドがあることです。だからこそそこで確実に delegate
インスタンスを消滅させることができる訳です。
やり方としてはかなりトリッキーですし、 UIKit がちゃんと Blocks 対応を進めればこの拡張カテゴリは不要になりますが、こうやって開発者が一歩先に進んで低いレイヤーから独自にフレームワークを拡張させることができるのは Objective-C プログラミングの面白いところだと思います。
ではまた次回、お会いしましょう。
GHUnitで単体テストをしてみよう
初めまして。プログラマのショウといいます。
現在、mixiの公式iPhoneアプリを担当しています。
今回は、iPhoneアプリ開発におけるGHUnitを用いた単体テストについて紹介したいと思います。
★ テストとは
本題に入る前に少しだけ、テストという概念について整理してみましょう。
ソフトウェアを開発する上での「テスト」という言葉は、「コンピュータのプログラムを実行し、正しく動作するかを確認する作業のこと」を指します。
そしてこの「正しく動作するかを確認する方法」として主に以下の2通りがあります。
・ ホワイトボックステスト
・ ブラックボックステスト
ホワイトボックステストとは、「命令網羅」「分岐網羅」「条件網羅」などの方式を用いて、プログラム内部の動作がプログラマの意図通りとなっているかを確認するものとなります。
これに対してブラックボックステストとは、プログラム内部に関係なく、外部から見て仕様通りの機能を持っているかを確認するものとなります。
★ 単体テストとは
では、今回のテーマである単体テストとは何でしょうか。
単体テスト(Unit Testing)とは、プログラム内部の個々のモジュール(部品)のみを対象としたテストを指します。
対象のモジュールが仕様書で要求された機能や性能を満たしているかどうかをテストするものであり、主にホワイトボックステストの手法が用いられます。
単体テストには、各プログラミング言語に用意されたxUnitフレームワークや、iPhoneアプリであればOCUnitフレームワークなどの単体テスト用ツールを用いるのが一般的となっています。
これらツールでは、個々のモジュールに対応した「テストコード」を書いて実行させることで、モジュールレベルでの不具合が存在しないかを確認することができます。
また、一度書いたテストコードは何度も実行できるため、定期的に全てのテストコードを実行させることでソフトウェアの品質を担保していく、というのも単体テストの目的のひとつとなります。
★ 単体テストの必要性
では「単体テストはやった方がよいのか?」を考えた時、その答えはどうでしょうか。
私はこれまで多くのエンジニアとこのような会話をしてきましたが、単体テストをやるべきという人の意見としては、
・ 「納品物に対する品質が担保できる」
・ 「メンテナンスしていく中でのデグレを防げる」
・ 「テスト前提のコードはプログラム設計が綺麗」
などがあり、逆に単体テストはやりたくないという人の意見としては、
・ 「テストコードを書く時間がない」
・ 「テストコードの保守をしていかければならない」
・ 「どこまでをどれだけやるかを明確にするのが難しい」
・ 「単体テストという項目を工数見積もりに載せにくい」
といった事を聞いたことがあります。
品質面でのメリットと引き換えに工数面や精神面でのデメリットもあり、それらがネックとなって単体テストに抵抗を感じるエンジニアも存在します。
故に単体テストの必要性については、メリット・デメリットを考え、それぞれの開発プロジェクトの状況下で必要か否かを検討するしかないのかなと、個人的に思います。
★ iPhoneアプリで単体テストをしてみよう
前置きが長くなってしまいましたが、本題に入っていきたいと思います。
単体テストの必要性などを整理してみましたが、mixi規模のサービスとなれば品質は最重要であり、iPhoneアプリとしても単体テストのメリットを享受しないわけにはいきません。
mixiの公式iPhoneアプリは単体テストにGHUnitと呼ばれるテスト用フレームワークを採用してします。
さらに、ソースコードをリポジトリサーバにコミットすると、Jenkinsが単体テストを自動実行し、テストが通った場合に自動ビルドが行われる、という仕組みを導入しています。
※この仕組みについては、次回以降の記事で紹介したい思います。
それではこのGHUnitの使い方について、簡単に紹介していきたいと思います。
★ GHUnitとは
GHUnitはMax OS X / iOS用の単体テストフレームワークであり、以下の特徴を持っています。
・ 非同期通信のテストが容易にできる
・ iPhone実機・シミュレータで動作確認ができる(実機でしか発生しないバグをキャッチできる)
XcodeにはデフォルトでOCUnitと呼ばれる単体テストフレームワークが付属していますが、GHUnitではOCUnitで出来ることに加え、上記の特徴を持っているのが魅力となっています。
★ GHUnitのダウンロード
以下URLから「GHUnitIOS-x.x.xx.zip」をダウンロードします。
https://github.com/gabriel/gh-unit/downloads
ダウンロードしたものを解凍したら、「GHUnitIOS.framework」というフォルダができます。
まずはこれをXcodeプロジェクトに組み込むまでのプロセスを解説していきます。
★ Xcodeプロジェクトの作成とGHUnitフレームワークの追加
Xcodeを起動し、新規プロジェクトを作成します。ここでは簡単なサンプルのため「Single View Application」を選択します。
プロジェクト名は「Sample」とし、ここでは各チェックボックスは全てOFFとします。
Xcodeプロジェクトが出来上がったら、GHUnit用のTargetをひとつ追加します。下部にある「Add Target」をクリックします。
デフォルトのソースコードは全て削除するため、「Empty Application」を選択します。
プロジェクト名は「Tests」とし、ここでも各チェックボックスは全てOFFとします。
新しく「Tests」グループがSampleプロジェクト内に作成されます。ここからTests-Info.plistとTests-Prefix.pch以外は不要となるため削除します。
次に、その下のグループの「Frameworks」を右クリックし、「Add Files to "Sample"」を選択、さきほどダウンロードした「GHUnitIOS.framework」を選択します。
「Add to targets」のTestsのチェックボックスをONにします。
「Frameworks」にGHUnitが追加され、使用可能となりました。
ここまできたら後ひと息です。最後に、Testsターゲットの「Build Settings」を選択し、「Other Linker Flags」の項目に、「-ObjC」を追加します。
次にTestsグループにmainメソッドのクラスファイルを追加します。ここでは「SampleTestMain」という名前で、以下の内容でソースコードを追加します。
SampleTestMain.m
#import void exceptionHandler(NSException *exception) { NSLog(@"%@\n%@", [exception reason], GHUStackTraceFromException(exception)); } int main(int argc, char *argv[]) { NSSetUncaughtExceptionHandler(&exceptionHandler); NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; int retVal = 0; if (getenv("GHUNIT_CLI")) { retVal = [GHTestRunner run]; } else { retVal = UIApplicationMain(argc, argv, nil, @"GHUnitIPhoneAppDelegate"); } [pool release]; return retVal; }
準備が整いました。ビルドターゲットに「Tests」、実行環境にiPhone Simulatorを選択し、起動してみましょう。
以下のようにGHUnitのアプリが起動すれば、組み込み完了です。
★ テストコードの追加と実行
環境が整ったため、実際にGHUnitを使って単体テストをしてみましょう。
Sampleグループにテスト対象のCalcというクラスファイルを追加し、「int型の引数同士を除算して切り上げた値を返却する」divideRoundUpメソッドを記述します。
Calc.h
#import @interface Calc : NSObject + (NSInteger)divideRoundUp:(NSInteger)numerator denominator:(NSInteger)denominator; @end
中身を実装します。
Calc.m
#import "Calc.h" @implementation Calc + (NSInteger)divideRoundUp:(NSInteger)numerator denominator:(NSInteger)denominator; { CGFloat value = (CGFloat)numerator / (CGFloat)denominator; return ceil(value); } @end
これでテスト対象となる機能ができました。次にテストを実行するクラスを作成します。
TestsグループにCalcTestというクラスファイルを追加します。
CalcTest.m
#import #import "Calc.h" @interface CalcTest : GHTestCase {} @end @implementation CalcTest // デフォルトはNOですが、UIのテストやメインスレッドに依存するテストを行う場合はYESにします。 - (BOOL)shouldRunOnMainThread { return NO; } // 本クラスが実行される前に呼び出されます。 - (void)setUpClass { } // 本クラスが終了された後に呼び出されます。 - (void)tearDownClass { } // 本クラスの各メソッドが実行される前に呼び出されます。 - (void)setUp { } // 本クラスの各メソッドが終了された後に呼び出されます。 - (void)tearDown { } // 「test~」というメソッド名にすることでテスト対象一覧に出力されます。 - (void)testDivideRoundUp { NSInteger num = [Calc divideRoundUp:100 denominator:3]; GHAssertEquals(num, 34, @"test"); } @end
さきほど追加したCalc.hをインポートし、GHTestCaseクラスを継承しています。
testDivideRoundUpメソッドがCalcクラスのdivideRoundUpメソッドに対応したテストメソッドとなります。
GHUnitにテストメソッドであることを伝えるため、テストメソッドの接頭語は「test〜」とする必要があります。
この状態でiPhoneシミュレータを起動し、ナビゲーションバーにあるRunボタンを押して、テストを実行してみましょう。
問題なくテストが通ったでしょうか。
testDivideRoundUpメソッドでは、divideRoundUpメソッド で100÷3を切り上げた数値を取得し、その答えが34となるかどうかをGHUnitで用意されたGHAssertEquals関数を使って確認しています。
NSInteger num = [Calc divideRoundUp:100 denominator:3]; GHAssertEquals(num, 34, @"test");
それでは、CalcクラスのdivideRoundUpメソッドの実装が、「切り上げ」ではなく「切り下げ」の値を取得してしまうよう、不具合を仕込んでみます。
Calc.mのdivideRoundUpメソッドの中のceil関数を、切り下げを行うfloor関数に変更します。
Calc.m
+ (NSInteger)divideRoundUp:(NSInteger)numerator denominator:(NSInteger)denominator; { CGFloat value = (CGFloat)numerator / (CGFloat)denominator; return floor(value); // ceilからfloorに変更しました }
それでは再度iPhoneシミュレータを起動し、テストを実行してみます。
Line:35 Reason:'33' should be equal to '34'. test
「35行目の値が'34'にならなければいけないのに'33'でした」と怒られ、想定と異なる値が返ってくる不具合を発見することができました。
以上が、GHUnitを使った単体テストの流れとなります。
★ まとめ
いかがでしたでしょうか。
今回は私たちがmixi公式iPhoneアプリで採用しているGHUnitの導入と使い方について、簡単に紹介しました。
GHUnitでは非同期通信のテストなど、秘めたるものがまだまだあります。本当はそちらも紹介していきたいのですが、そこまでの需要があるかどうかは少し謎なため、はてブのブックマーク数でも見て決めようと思います。
それでは、また次回お会いしましょう!
※本記事では執筆時点で最新のXcode 4.3.2を使用しています。
絵文字だョ! 符号化文字集合(後編)
同僚の女性からクッキーをすすめられても、「サードパーティークッキーは拒否します」とキッパリお断り申し上げたiPhoneアプリ開発担当の七尾です。というか、どう考えてもホワイトデーの(ry
さて先週に引き続き、iOS開発でUnicode絵文字を扱う際の注意点について書いていこうと思います。
Combining Character/結合文字
サロゲートペアの他にも同様に気をつけなければいけないのが、結合文字です。
アルファベットに対しての修飾文字を付けたり、数字を四角で囲ったりした文字があります。
そういった文字は結合文字と呼ばれ、iPhoneで入力できる文字でいうと、
1を四角で囲った文字 = 0x31 0x20E3
2を四角で囲った文字 = 0x32 0x20E3
というようになります。
結合文字の文字数を取りたい場合は、特定の修飾文字を読み飛ばせば良いだけなので、
以下のようにさらっと対応することが可能です。
unichar c = [emoji characterAtIndex:i]; if (c == 0x20E3) { // 囲い文字 なので、文字数カウントしない } else { ++count; }
Regional Indicator/国旗絵文字
絵文字の文字数を取る上で最も面倒なのが、国旗絵文字です。なぜなら国旗絵文字はサロゲートペアによる合字だからです。
Unicodeへ絵文字追加の提案がされた時にベースとなったのは携帯電話3キャリアの絵文字ですが、その中には10ヶ国の国旗の絵文字が含まれています。
しかしUnicodeは国際規格なので、その10ヶ国分だけを収録するわけにはいかない。。。というのは容易に想像が付きます。
そこで考案されたのが、国旗用にアルファベットの範囲を決め、日本ならJとP、アメリカならUとS、と言ったように、アルファベット2文字の組み合わせで国旗を表すようにしようという考えです。
上記の案は実際にUnicodeに取り入れられ、その国旗用のアルファベット文字はRegional Indicatorと呼ばれています
図の4つの国旗はiPhoneで入力できる絵文字の一部ですが、確かに2つの文字を合わせたものになっています。
具体的にはU+1F1E6(UTF16: 0xD83C 0xDDE6)からU+1F1FF(UTF16: 0xD83C 0xDDFF)までです。
全てサロゲートペアの文字ですね。本当にありがとうございました。
ここまで調べた段階ですでにグッタリしていまい、何もやる気は起きないわけですが、
国旗絵文字だけをカウントする処理をどうにか実装しようとすると以下のような感じでしょうか。
unichar c = [emoji characterAtIndex:i]; if (0xDDE6 <= c && c <= 0xDDFF) { // 国旗絵文字のLow Surrogatesなので、工夫する :) if (YES==isRegionalIndicator) { // 2文字目のRegional IndicatorのLow Surrogatesは // 1つ手前のHigh Surrogates をカウントしてしまっているので、 // その分デクリメントする isRegionalIndicator = NO; --count; } else { // 1文字目のRegional Indicatorはカウントせず // 次のRegional IndicatorのLow Surrogates為に // フラグを立てておく isRegionalIndicator = YES; } } else { ++count; }
しかし実際には絵文字全般を1文字としてカウントしたい場合がほとんどなので、 他のサロゲートペアのカウントも対応するとなると、国旗判定フラグを利用するなりして工夫する必要がありそうです。
以上を踏まえたNSStringでのきちんとした実装については後日絵文字周りの処理をまとめてgithubに上げたいと思います。今日はこれで勘弁してください。