Cocoa/Objective-C

なぜ iPhone プログラミングではデリゲートを多用するのか

iOS / iPhone プログラミングでは、デリゲートをよく見かけることになります。どうしてこんなに見かける羽目になってしまったのでしょうか。それを考えてみることにします。

まず1つ。iPhone アプリケーションでもっとも基本的なユーザーインターフェースは、おそらくテーブルビュー ( UITableView )になるのではないでしょうか。このテーブルビューの実装では、少なくとも2つのデリゲートを実装することになります。それは、UITableViewDataSource と UITableViewDelegate であり、1つのクラスで両方のデリゲートを実装してしまえば、1つのクラスしかできませんが、それでもテーブルビュー1つにつき、2つのデリゲートを実装する必要があるのです。

そして2つ目。アプリケーションにも依りますが、 NSURLConnection ではデリゲートで通信のやりとりをします。つまり、ネットワークから何かリソースを取得するようなプログラムの場合、この NSURLConnection によってデリゲートを使う必要が出てくるわけです。

他にも UIAlert など、デリゲートが出てくる場面はたくさんあります。その結果、iOS / iPhone プログラミングでは、デリゲートを多用する必要が出てきてしまっているのでしょう。

一方で、OS X はどうなっているのかと言えば、そもそも UI の基本がテーブルビューとは限らないので、iOS ほどテーブルビューを使いません。そして何よりも、 OS X では Cocoa Biding が利用できるわけで、outlet / action 接続がほとんど無くなるだけでなく、テーブルビューでデータソースをデリゲートにする必要もなくなり、デリゲートの数が少なくなります。

iOS / iPhone で Cocoa Binding が無いのは、おそらくオーバーヘッドが大きいし、バインドして値を同期させるほどの処理性能が iPhone にはないからだと考えておりますが、よりデバイスが進化した数年後には、Cocoa Binding が iPhone でも使えるようになっていてもおかしくはないと思います。

Objective-C の名前空間について

Objective-C には名前空間がありません。そのことについて、不満を述べるプログラマが少なからず居るようですが、私はそんなことは思いません。なぜなら、名前空間は便利こそあれど、本質的にプログラミングに関係することではないと思っているからです。

Objective-C のランタイムシステムや Cocoa フレームワークの名前は、衝突を防ぐために古典的な方法がとられているのは、すぐにわかると思います。それは、名前の前に接頭辞を付けることです。たとえば、NSObject であったら、NS の部分がそれにあたります。

もし名前空間の機能があれば、おそらくファイルの先頭で名前空間の使用を宣言すれば、それ以降に接頭辞を付けることなくクラス名を書くことができるでしょう。これによって、記述が少なくなり、打つ手間も省けるでしょう。ただし、Objective-C の命名規則では、先頭の二文字が省略できるにすぎませんが。要するに、名前空間があったとしても、それほど便利になるとは思えないというところです。Objective-C は、Cから比較的簡素な拡張のされ方をしている言語なので、極力言語に機能を追加しないほうがいいと、私は考えています。

そうは言うものの、無いものは不便と感じる方も居ますので、気休め程度に名前空間が無いメリットを紹介します。

それは、クラス名がユニークなので、とても検索しやすいことです。クラス名が String であったら、検索したときにほかの言語の情報が出てくるかもしれませんが、 NSString ならば、簡単に NSString の情報を探し出すことができます。ついでに言うと、Objective-C の長いメソッド名も、打つのは大変であれど、検索しやすくていいところもあります。逆に検索結果が少なすぎて絶望することもありますが。

このように、名前空間が無いからこそのメリットもありますので、そんなに悲観すべきことではないと思います。

NSPredicate の使い方

NSPredicate の話を求めて、このブログへ訪れる方が多いようなので、NSPredicate の使い方を紹介しようと思います。

NSPredicate は、NSArray で要素をフィルターするための条件を表したり、Core Data で取ってくるデータの条件を表すためのクラスです。

NSPredicate を作成する方法として最も簡単なものが、predicateWithFormat です。これを用いると、NSPredicate 独自の記法ではありますが、自然な記述で条件を書くことができます。

Predicate の条件式の基本形は、”keyPath operator literal” です。それぞれには、次のようなものが入ります。

keyPath
キー値コーディングのキーパス。この値が条件の判定に利用されます。self 指定すると、そのオブジェクト自体の値が利用されます。(selfもキー値コーディングです)
operator
比較演算子が入ります。数値であれば、=, < , >, < =, >= 等、文字列であれば =, contains, like, startsWith, endsWith, in 等が使用できます。
literal
定数値を入れます。キーパスとの比較に用いられます。数値は直接書き、文字列の場合は ‘ ‘ や ” ” で括ります。また、配列を { e0, e1, … } のような形式で記述できます。

また、メソッド名に Format があるように、NSLogと同様に書式指定子が使用できます。ただし、Predicate 用に追加されている書式指定子 %K があります。これは、keypath の部分を指定する書式指定子で、キーパスを入れるところには %@ ではなく、 %K を使うことに注意しなければなりません。

具体的な例を見れば、すぐにわかると思いますので、いくつかの例を示します。

NSArray *strings = [NSArray arrayWithObjects:@"first", @"second", @"third", nil];
NSPredicate *predicate;

// 単純な比較
predicate = [NSPredicate predicateWithFormat:@"self = 'first'"];
NSLog( @"%@", [strings filteredArrayUsingPredicate:predicate] );
// { first }

// 書式指定子を利用した比較
predicate = [NSPredicate predicateWithFormat:@"%K = %d", @"length", 5];
NSLog( @"%@", [strings filteredArrayUsingPredicate:predicate] );
// { first, third }

// in 演算子の利用と、配列のリテラル値の入力方法
predicate = [NSPredicate predicateWithFormat:@"self in %@", [NSArray arrayWithObjects:@"first", @"second", nil]];
NSLog( @"%@", [strings filteredArrayUsingPredicate:predicate] );
// { first, second }

// contains 演算子の利用
predicate = [NSPredicate predicateWithFormat:@"%K contains %@", @"self", @"i"];
NSLog( @"%@", [strings filteredArrayUsingPredicate:predicate] );
// { first, third }

// like演算子と1オプションの利用
predicate = [NSPredicate predicateWithFormat:@"%K like1 %@", @"self", @"FIRST"];
NSLog( @"%@", [strings filteredArrayUsingPredicate:predicate] );
// { first }

また、not, and, or 等の演算子を利用できます。

predicate = [NSPredicate predicateWithFormat:@"not ( 1 <= length and length <= 5 )"];
NSLog( @"%@", [strings filteredArrayUsingPredicate:predicate] );
// { second }

演算子やPredicateの作成方法は、他にもあります。詳しくは、Predicate Programming Guide を御覧ください。

参考資料
Predicate Programming Guide
NSPredicate Class Reference

NSURLConnection+Blocks

まだあまり相手にされていない感じの Blocks ですが、使うことができるシーンは結構あります。

NSURLConnection を使用すると、簡単にWEBからリソースを取得できますが、多少記述が多くなってしまうのと、デリゲートによって処理が散らばってしまうのが問題です。そこで、Blocks を使って、よりモダンな書き方ができるようにしてみます。

今回は NSURLConnection にカテゴリとして追加することにしました。使い方はこんな感じになります。

 NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.lifeaether.com/"]];
[NSURLConnection sendRequest:request completeBlock:^( NSData *receivedData ) {
    NSString *html = [[NSString alloc] initWithData:receivedData encoding:NSUTF8StringEncoding];
    NSLog( @"%@", html );
    1;
} errorBlock:^( NSError *error ) {
    NSLog( @"%@", error );
}];

completeBlock がリソースの取得に成功したときに呼び出されるブロックです。引数に受信したデータが入っています。errorBlock が失敗したとき呼び出されるブロックです。引数にエラー内容が入ります。

カテゴリの実装です。
NSURLConnection+Blocks.h

#import <Foundation/Foundation.h>

@interface NSURLConnection (BlocksAddition)

+ (void)sendRequest:(NSURLRequest *)request completeBlock:( void (^)( NSData *receivedData ) )complete errorBlock:( void (^)( NSError *errror ) )error;

@end

NSURLConnection+Blocks.m

#import "NSURLConnection+Blocks.h"

@interface NSURLConnectionBlocksAddtionDelegate : NSObject {
@private
    NSMutableData *receivedData;
    void (^completeBlock)( NSData *receivedData );
    void (^errorBlock)( NSError *error );
}

- (id)initWithCompleteBlock:( void (^)( NSData *receivedData ) )compelete errorBlock:( void (^)( NSError *error ) )error;

@end

@implementation NSURLConnectionBlocksAddtionDelegate

- (id)initWithCompleteBlock:( void (^)( NSData *receivedData ) )compelete errorBlock:( void (^)( NSError *error ) )error
{
    self = [super init];
    if ( self ) {
        receivedData = [[NSMutableData alloc] init];
        completeBlock = [compelete copy];
        errorBlock = [error copy];
    }
    return self;
}

- (void)dealloc {
    [errorBlock release];
    [completeBlock release];
    [receivedData release];
    [super dealloc];
}

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
    [receivedData setLength:0];
}

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
    [receivedData appendData:data];
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
    completeBlock( receivedData );
}

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
    errorBlock( error );
}

@end

@implementation NSURLConnection (BlocksAddition)

+ (void)sendRequest:(NSURLRequest *)request completeBlock:( void (^)( NSData *receivedData ) )complete errorBlock:( void (^)( NSError *errror ) )error
{
    id delegate = [[[NSURLConnectionBlocksAddtionDelegate alloc] initWithCompleteBlock:complete errorBlock:error] autorelease];
    [self connectionWithRequest:request delegate:delegate];
}

@end

特に説明する部分はないかと思われますが、疑問が出そうなところを少し解説します。

sendRequest: メソッド内で NSURLConnectionBlocksAddtionDelegate インスタンスに autorelease していて、開放されるんじゃないかと思われる人がいるかもしれませんが、NSURLConnection は例外的に delegate を retain するので、開放される心配はありません。

initWithCompleteBlock: メソッド内で、引数のブロックを copy していますが、これはブロックが通常はスタック領域に確保され、関数から抜けだすと消失してしまうからです。copy することによってそれを回避します。それによって、releaseする必要も生じます。

Blocks の戻り値の型に注意

最近お気に入りの Blocks ですが、ハマったところがあったので、それを紹介しようと思います。

例えば、次のような Block を宣言すると型が違うと言われてエラーが出ます。

float (^zero)() = ^{
        return 0.0;
     };

NSLog( @"%f", zero() );

さて、何がいけなかったのでしょうか。実は、0.0というのが float 型だと思われてないのです。つまり、キャストするか f を付けることで float 型と思わせることで、このエラーを取り除くことができます。

float (^zero)() = ^{
        return 0.0f;
     };

NSLog( @"%f", zero() );

このように、 Block の戻り値は厳密に型チェックされるようで、暗黙型変換はしないようです。なぜか Blocks を使っていて型エラーが出てしまう時には、型を確かめるようにしましょう。

なぜ Objective-C はメッセージ式なのか

Objective-C が奇妙に見える大きな要因のひとつは、明らかにメッセージ式の存在だと思います。なぜ Objective-C はメッセージ式という奇妙な式を用いてメソッドを呼び出すようになったのでしょうか? Objective-C の作者にどういった意図があったのかは知りませんが、私なりに Objective-C がメッセージ式でなければいけない理由を見つけていますので、そのお話をしようと思います。

まず、他の多くの言語で採用されている、 . によるメンバアクセスによるメソッド呼び出しを考えてみましょう。

anObject.method( );

みなさんはこれをどう解釈するでしょうか? インスタンス anObject の method メソッドを実行する。そう解釈するのが普通だと思われます。なぜなら、 . は、多くの場合メンバへのアクセスを意味するもので、 anObject のメンバである method メソッドを呼びだすと考えることができるからです。

call method

それでは、こちらはどうでしょうか?

[anObject method];

みなさんはこれをどう解釈するでしょうか? anObject の method メソッドを実行するでしょうか?そう思った方は、まだ Objective-C に不慣れな証拠です。これは、 anObject に method メッセージを送信すると解釈すべきです。

send message

Objective-C では、オブジェクトがメッセージを受け取ること、すなわちメソッドの実行というわけではありません。現実としては、対応するメッセージの実装が呼び出されていますが、それをメソッド呼び出しと呼ぶことには、再考の余地があります。

例えば、Objective-C では、デリゲートがあります。デリゲートは、メッセージを受け取ったオブジェクトが、デリゲートオブジェクトにメッセージを転送し、そのデリゲートのメソッドに委譲して処理させるための仕組みです。この場合、誰がメソッドを実行していますか?デリゲートオブジェクトです。すなわち、必ずしもメッセージを受け取ったオブジェクトが、実際に処理するメソッドを実行しているわけではないのです。メッセージを受け取ったオブジェクトは、メッセージを転送するという処理のメソッドを実行しているのです。

例えば、 Objective-C では、オブジェクトにどんなメッセージを投げることも許容しています。 . によるメンバへのアクセスならば、メンバが無い時点でエラーを出すべきであり、コンパイル時にエラーを出します。しかし、 Objective-C の場合、たとえ実装のないメッセージでも、コンパイル時は警告のみで、コンパイルすることは可能です。これは、オブジェクトはどんなメッセージも受け取るということです。

受け取ったメッセージに対応するメソッドの実装がなかった場合、そのメッセージはどうなるでしょうか?何もしなければ例外が発生するようにできていますが、 Objective-C ではそのようなメッセージを処理する仕組みが用意されています。 methodSignatureForSelector: と forwardInvocation: を適切にオーバーライドすることによって、対応する実装のないメッセージに対して、処理をすることができます。処理の内容は、代わりに適当なメソッドを実行させるようにしたり、別なオブジェクトにメッセージを転送したり、例外を発生させたりと、さまざまな処理を記述することができます。

Objective-C では、メッセージとメソッドを分けて考えるべきだと思います。 . によるメソッド呼び出しの記述は、まさにそのオブジェクトのメソッド実行を意味しますが、オブジェクトへのメッセージ送信は、必ずしもメソッド実行と結びつかないわけです。メッセージ送信のイメージは、メソッド実行ではなく、オブジェクト同士の会話を想像すべきです。メッセージ送信先の相手が、どのようにメッセージを解釈しようと、適切に返事をしてくれさえすれば、何も気にすることはないのです。

メッセージ式に慣れてくれば、この [ ] を見ると、オブジェクトにメッセージを投げているように見えてくるはずです。そして、 . とは違う意味があることに気がつくことでしょう。

Objective-CとC言語の速度比較なんて無意味

Objective-C と C言語の速度差を比較した話がありましたが、そもそもCの完全上位互換である Objective-C と比較しても、何も意味はないと思います。例え Objective-C を使っていたとしても、速度が必要なところは C で記述するのが普通だからです。それを簡単にできるが Objective-C のいいところでもあります。

私の意見はそれだけなのですが、Objective-C と C のコードが、到底同じことをやっているようには見えなくて納得がいかなかったので、同じような動作になるようにCのコードを書きなおしてみました。

- (void)draw {
	glColor4f(0.8, 1.0, 0.76, 1.0);
    glLineWidth(4.0f);

#ifdef USE_OBJC
	for (int i = 0; i < screenWidth; i++) {
		NSNumber *tempNumber = [lineArrayX objectAtIndex:i];
		CGFloat tempFloat = [tempNumber floatValue];
		if (tempFloat > screenWidth) {
			[lineArrayX replaceObjectAtIndex:i withObject:[NSNumber numberWithFloat:0]];
		} else {
			[lineArrayX replaceObjectAtIndex:i withObject:[NSNumber numberWithFloat:(tempFloat + 2)]];
		}

		CGFloat y = 8.0f * cosf(i / 60.0f * 3.14);
		ccDrawLine( ccp(screenWidth - tempFloat, 40 + y), ccp(screenWidth - tempFloat, -screenHeight) );
	}
#else
	for (int i = 0; i < screenWidth; i++) {
		CGFloat tempFloat;
		CFNumberGetValue( CFArrayGetValueAtIndex( lineArrayX, i ), kCFNumberFloat32Type, &tempFloat );
		CGFloat zero = 0.0;
		if (tempFloat > screenWidth) {
			CFRelease( CFArrayGetValueAtIndex( lineArrayX, i ) );
			CFArraySetValueAtIndex( lineArrayX, i, CFNumberCreate( NULL, kCFNumberFloat32Type, &zero ) );
		} else {
			CGFloat temp2Float = tempFloat + 2;
			CFRelease( CFArrayGetValueAtIndex( lineArrayX, i ) );
			CFArraySetValueAtIndex( lineArrayX, i, CFNumberCreate( NULL, kCFNumberFloat32Type, &temp2Float ) );
		}

		CGFloat y = 8.0f * cosf(i / 60.0f * 3.14);
		ccDrawLine( ccp(screenWidth - tempFloat, 40 + y), ccp(screenWidth - tempFloat, -screenHeight) );
	}
#endif
}

NSAutoreleasePool を使っているか否かの差のみで、ほぼ同じようなことをしているコードになっています。

結果、iPhone 3G iOS 4.1 で Objective-C では約 27 FPS 、C言語では約 35 FPS でした。

さらに同じコードとなるように、Objective-C のコードにも手を加えると約 25 FPSとなり、約 10 FPS の差が出ました。

- (void)draw {
	glColor4f(0.8, 1.0, 0.76, 1.0);
    glLineWidth(4.0f);

#ifdef USE_OBJC
	for (int i = 0; i < screenWidth; i++) {
		NSNumber *tempNumber = [lineArrayX objectAtIndex:i];
		CGFloat tempFloat = [tempNumber floatValue];
		if (tempFloat > screenWidth) {
			[[lineArrayX objectAtIndex:i] release];
			[lineArrayX replaceObjectAtIndex:i withObject:[[NSNumber alloc] initWithFloat:0]];
		} else {
			[[lineArrayX objectAtIndex:i] release];
			[lineArrayX replaceObjectAtIndex:i withObject:[[NSNumber alloc] initWithFloat:(tempFloat + 2)]];
		}

		CGFloat y = 8.0f * cosf(i / 60.0f * 3.14);
		ccDrawLine( ccp(screenWidth - tempFloat, 40 + y), ccp(screenWidth - tempFloat, -screenHeight) );
	}
#else
	for (int i = 0; i < screenWidth; i++) {
		CGFloat tempFloat;
		CFNumberGetValue( CFArrayGetValueAtIndex( lineArrayX, i ), kCFNumberFloat32Type, &tempFloat );
		CGFloat zero = 0.0;
		if (tempFloat > screenWidth) {
			CFRelease( CFArrayGetValueAtIndex( lineArrayX, i ) );
			CFArraySetValueAtIndex( lineArrayX, i, CFNumberCreate( NULL, kCFNumberFloat32Type, &zero ) );
		} else {
			CGFloat temp2Float = tempFloat + 2;
			CFRelease( CFArrayGetValueAtIndex( lineArrayX, i ) );
			CFArraySetValueAtIndex( lineArrayX, i, CFNumberCreate( NULL, kCFNumberFloat32Type, &temp2Float ) );
		}

		CGFloat y = 8.0f * cosf(i / 60.0f * 3.14);
		ccDrawLine( ccp(screenWidth - tempFloat, 40 + y), ccp(screenWidth - tempFloat, -screenHeight) );
	}
#endif
}

NSNumber と CFNumber 実は同じなのですが、NSNumber のイニシャライザは、CFNumberCreate より多少簡易的になっているので、ラップされているものだと思われる。そういう多少の違いはあるにせよ、Objective-C のメソッド呼び出しによるオーバーヘッドはこの程度なのだと思います。

修正したファイル

iPhone 開発雑感

最近 iOS SDK で iOS (iPhone) のアプリケーションを作成しているのですが、一通りの基本的なインターフェースは構成できるようになったので、その感想でも書きます。

Mac などのデスクトップアプリケーションでの UI の構成要素は Window と View 両方が主役ですが、iOS(iPhone)では一枚の Window に View を貼りつけていく形になります。そうなる理由は簡単で、あの小さな画面には Window が一枚で十分なのです。

iOS でもっともよく用いられる View は、おそらく TableView です。iOS では、この TableView が最も基本的な View となっています。デスクトップアプリケーションの Table というと、リストを表示することが主な目的となりますが、iOS の場合はそれだけでなく、さまざまな場面で用いられています。

例えば、環境設定の画面がありますが、あれもまさに TableView です。環境設定くらいならば、まだ TableView の原型を留めていますが、カスタムの TableViewCell を用いた TableView で構成されていたりすると、一見 TableView に見えない画面も、TableView で構成できます。

iPhone は、画面が小さいので、たくさんの情報を一度に表示することはできません。したがって、画面の遷移が多くなるわけですが、iOS では NavigationView でそれを見事に解決しています。この NavigationView を用いると、とても簡単に画面を遷移させることができ、View 間の移動をとても簡潔にコーディングすることができます。

私がもっとも iOS SDK に感心を抱いたのは、このような画面遷移の仕組みが用意されているところであり、それらの要素になる TableView や通常の View 、さらに Navigation の外側の TabBar がありますが、それらの連携がとてもスムーズな点です。

ユーザー側に洗練されたインターフェースを提供するだけでなく、デベロッパ側にも洗練されたインターフェースを提供できるのは、とても素晴らしいことだと思います。

iPhone Developer Program に早く参加したいです

ようやく iPhone 開発するきっかけができたので、開発しつつ iPhone Developer Program に申し込んだのですが、想像よりも手続きが難航していて困っています。

日本語表記のアカウントだと問題が発生する噂は聞いていたので、英語表記のアカウントを作成したのですが、支払い手続きのところでアップルストアに飛ばされるようになっていたので、結局住所は日本語表記にせざるを得ない状況に。さらに、名前はローマ字表記のままだったので、支払い者の情報が日本語表記とローマ字表記が入り乱れる不思議な状況に。

とりあえず、銀行振込で完了し、アクティベーションコードはすぐに届いたものの、支払い者とアカウント情報が一致しないと言われる有名なエラーにひっかかります。

そして Contact us からサポートメールを送ったのですが、返事は1週間後でした。もうちょっと早いと思ってたので、ちょっと残念です。

ようやくアクティベーションが完了するかと思いきや、手動でアクティベーションする際に必要な、アップルストアの購入履歴のページがエラーで表示されず、アクティベーションできず。別のページから同じページへ飛ぶと、メンテナンス中なので後日アクセスしてください、とのことでした。

そして今日、4日経ってもメンテナンスが完了しないので、またサポートにメールを送りました。そもそも、他のアカウントでは正しく表示できるので、メンテナンスではなくて、何かエラーがおきてる気がしますが、、。

そんなこんなで、未だに実機でテストできません。もうちょっと手続きが楽になるといいのですが、どうにか改善されないものでしょうか。

Objective-C クイックリファレンスを公開します

最近 Objective-C が注目されてきているので、ここらへんでクイックリファレンス的なものを書いてみました。まだすべての項目は書いていませんが、半分くらい書けたので公開します。

これを書き終えたら、次は Interface Builder のほうを書いてみたいです。