— boreal-kiss.com

Archive
Tag "Objective-C"

イントロダクション

Grand Central Dispatch (GCD)にはforループをマシンのコア数に応じて並列処理してくれる便利な関数 dispatch_apply(size_t, dispatch_queue_t, void) が存在する。

void dispatch_apply(
	size_t iterations,
	dispatch_queue_t queue,
	void (^block)(size_t));

引数は前から順番に、forループのイテレーション回数、タスク送信先のキュー、各イテレーションで行われるタスク、である。この関数はforループの各イテレーションごとにキューにタスクを送っている。したがって、もしタスクの処理時間がキューにタスクを送信するためにかかる時間(オーバーヘッド)より小さくなるような場合には注意が必要となる。普通にforループをまわす時よりも処理が遅くなる可能性があるからだ。具体例を見てみよう。

実行環境

Mac OS X 2.4 GHz Intel Core 2 Duo (CPU2個)

forループ vs. GCD その1

以下のような「何もしない」blockをforループでまわして処理にかかった時間を計測してみる。

void (^block)(int i) = ^(int i){
 
};

比較するのは普通のforループ処理を行う関数 loop_normal(int, void)

static void loop_normal(int count, void (^block)(int)){
	for (int i=0; i<count; i++){
		block(i);
	}
}

とGCDを用いた場合の関数 loop_normal_gcd(int, void) である。

static void loop_normal_gcd(int count, void (^block)(int)){
	void (^block_gcd)(size_t i) = ^(size_t i){
		block(i);
	};
 
	dispatch_queue_t queue 
		= dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
	dispatch_apply(count, queue, block_gcd);
}

比較した結果が下図になる(横軸はイテレーション回数、縦軸が関数の処理にかかった時間)。これを見るとGCDを用いた場合の方が(格段に)遅くなっていることがわかるだろう。この原因は「何もしない」タスクを完了するための時間に比べてキュー送信のオーバーヘッドが大きいことによる。

for loop vs GCD

それではこのオーバーヘッドが無視できるような構造にするにはどうすればよいだろうか。ひとつの方法はループにストライドを導入することである。

ストライドの導入

この方式はストライドで定義される整数値分の回数のイテレーションを一回のイテレーションに押し込んでしまう方法だ。これによりforループのイテレーション回数が減り、かわりに一回のイテレーションにおけるタスク量を増やすことができる。具体的に見てみよう。まず普通のforループを行う関数 loop_normal(int, void) がある。これはcount回のイテレーションを行う。

static void loop_normal(int count, void (^block)(int)){
	for (int i=0; i<count; i++){
		block(i);
	}
}

次に上記関数と全く同じ処理を行う関数が以下の loop_stride(int, int, void) である。ただし整数 stride (stride < count) を導入することで関数内部のループ回数が (count / stride) になっていることに注目してもらいたい。イテレーション回数が減った分イテレーション一回のタスク量が増えていることもわかるだろう。

static void loop_stride(int count, int stride, void (^block)(int)){
	int iMax = count / stride;
	int remainder = count % stride;
 
	void (^block_stride)(int i) = ^(int i){
		int j = i * stride;
		int jMax = j + stride;
 
		if (i == iMax - 1){
			jMax += remainder;
		}
 
		while (j < jMax) {
			block(j);
			j++;
		}
	};
 
	for (int i=0; i<iMax; i++){
		block_stride(i);
	}
}

ストライドを導入すればイテレーションの一回のタスク量を増やせることがわかった。それでは実際にGCDに放り込んで結果を見てみよう。

forループ vs. GCD その2

比較したのは普通のforループを行う関数 loop_normal(int, void)

static void loop_normal(int count, void (^block)(int)){
	for (int i=0; i<count; i++){
		block(i);
	}
}

とストライドを導入して一回のタスク量を増やした関数 loop_stride_gcd(int, int, void) である。

static void loop_stride_gcd(int count, int stride, void (^block)(int)){
	int iMax = count / stride;
	int remainder = count % stride;
 
	void (^block_gcd)(size_t i) = ^(size_t i){
		int j = i * stride;
		int jMax = j + stride;
 
		if (i == iMax - 1){
			jMax += remainder;
		}
 
		while (j < jMax) {
			block(j);
			j++;
		}
	};
 
	dispatch_queue_t queue 
		= dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
	dispatch_apply(iMax, queue, block_gcd);
}

そして両者の引数に「何もしない」blockを渡し実行時間を比較した。

void (^block)(int i) = ^(int i){
 
};

ここではイテレーションの回数は100,000回に固定し、ストライドの値を変化させて両者を比較した。結果は以下の図になる(横軸はストライドの値、縦軸は関数の処理にかかった時間)。ストライドの値が大きい場合はGCDを用いた場合の方が処理が早くなっていることがわかる。これらはつまりキューにタスクを送る際のオーバーヘッドが全体として無視できる程度になっていることを意味する。

for loop vs GCD (stride)

まとめ

GCDの恩恵を受けるためにはタスクは適度に重い(処理に時間がかかる)ものである必要があることがわかった。タスクが軽すぎる場合、GCDを利用することでかえって普通のforループよりも処理が遅くなる場合がある。これはGCDのオーバーヘッドによる。このような場合にはforループにストライドを導入することでイテレーション回数を減らし一回のタスクを重くすることで解決できる。

Read More

Grand Central Dispatch

OSX 10.6から導入されたGrand Central Dispatch (GCD)は複数タスクを処理するための便利な仕組みのことで、これを使うとマルチコアの恩恵を簡単に受けることができる。複数同時処理といった仕組みとしてスレッドがあるが、GCDの場合(スレッドを作成する時・制御する時のような)面倒くさい手順を一切踏まなくてよくなる。さらにシステム側でCPUを適切に利用する工夫がされているので(CPU1個なら1個、2個なら2個、8個なら8個勝手に使ってくれる)実行環境を気にしなくてよい、という利点が挙げられる。詳細は以下のドキュメントが非常に参考になる。

さて今回、CPUが複数あるマシンでGCDの恩恵を受けているかどうかを簡単な計算で確認したので以下に結果を載せておく。GCDにはforループを簡単に並列化する仕組みがあるのでそれを利用した。なお計算を行ったマシンは2.4 GHz Intel Core 2 Duo。

forループの並列化

以下のようなforループがあるとする。

for (int i=0; i<count; i++){
	//Do some work.
}

ループ内のi回目の処理が他と独立したタスクである場合GCDのdispatch queueを用いて並列処理が可能になる。以下は上記forループと同じ仕事を行う。

//GCD way.
void (^block)(size_t i) = ^(size_t i){
	//Do some work.
};
 
dispatch_queue_t queue 
	= dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(count, queue, block);

ここで

void (^block)(size_t i)

はblockと呼ばれるApple独自のC言語の拡張である。これは簡単に言うと一連の作業をまとめてオブジェクト化したものであり1、GCDとは親密な関係にある。block自体も簡単で便利な仕組みなので、興味があるひとは以下のドキュメントを参考にしてほしい。

さて上記の場合、blockに記述された内容がforループ内の処理に相当し、これをGCDのdispatch_apply()の引数として渡してシステム側で処理してもらっている。記述が簡単な点にも注目してもらいたい。

計算結果

普通のforループとdispatch queueを用いた場合を比較したものを載せておく。forループ内の1回の処理時間(t_task)を横軸、ループ終了までにかかった時間を縦軸にとってある。ここでループの回数(count)はt_taskの値ごとに異なっており

t_task x count = 10 sec.

となるように設定してある。例えば図の一番左のバーの場合、ループ内の一回の処理時間を0.00001秒、ループの総数を1000000回としてある(全処理時間は0.00001 x 1000000 = 10秒)2。試行範囲に限って言えばdispatch queueを用いた方が2倍ほど早いことがわかる。2倍という数字はCPU2個使った並列化、ということだろう。設定によっては普通のforループの方が早くなるようなことがあるかと思われたがそのようなこともなかったので、並列化が可能であればGCDはどんどん使った方が良さそうだ。

確認用プログラム

上記計算に使用したプログラム。ループ内の処理時間の変更は指定時間スリープさせることで実現させてある。gettimeofday_sec()はマイクロ秒測定用の関数でC言語: 実行時間測定の方法より拝借した。

#import <Foundation/Foundation.h>
#include <sys/time.h>
 
double gettimeofday_sec();
static void test_normal_loop(int count, NSTimeInterval sleep);
static void test_dispatch_que(int count, NSTimeInterval sleep);
 
int main (int argc, const char * argv[]) {
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
 
	const NSTimeInterval TEN_SEC = 10.0;
 
	for (int count = 10; count < 1e7; count*=10){
		NSTimeInterval sleep = TEN_SEC / count;
		test_normal_loop(count, sleep);
		test_dispatch_que(count, sleep);
	}
 
    [pool drain];
    return 0;
}
 
double gettimeofday_sec(){
    struct timeval tv;
    gettimeofday(&tv, NULL);
    return tv.tv_sec + (double)tv.tv_usec*1e-6;
}
 
static void test_normal_loop(int count, NSTimeInterval sleep){
	double t1 = gettimeofday_sec();
 
	for (int i=0; i<count; i++){
		[NSThread sleepForTimeInterval:sleep];
	}
 
	double t2 = gettimeofday_sec();
 
	printf("%f\n", t2 - t1);
}
 
static void test_dispatch_que(int count, NSTimeInterval sleep){
	double t1 = gettimeofday_sec();
 
	void (^block)(size_t i) = ^(size_t i){
		[NSThread sleepForTimeInterval:sleep];
	};
 
	dispatch_queue_t queue 
		= dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
	dispatch_apply(count, queue, block);
 
	double t2 = gettimeofday_sec();
 
	printf("%f\n", t2 - t1);
}

Footnotes

  1. 仕組みとしてはNSInvocationに似ているがblockは特定のメソッドだけに固執しておらず、記述された内容をprocedualに実行できる []
  2. 縦軸のループ処理時間が10秒以上になっているものがあるのはループを行う際のオーバーヘッドの累積によるもの。 []
Read More

例えば、NSProxyのサブクラスにメッセージのフォワーディングをさせたい時1などにどうしてもこの警告が表示されてしまう。存在しないメソッドを呼んでいるので警告が出るのは当たり前だが放置しておくにはうっとおしい。この警告、実は簡単に消すことができる。

Solution

“…may not respond to…”が表示されるクラスのカテゴリを新規に作成し(インターフェイスのみ)、その中に”…may not respond to…”が表示されるメソッドを定義してやる。インターフェイスは”…may not respond to…”が表示されるファイルの一番上にでも書いておけばよいだろう。

Example

以下の例ではMyObjectクラスの-[MyClass doSomething]をメインスレッドで実行する際にNSProxyのフォワーディングを利用している。-[MyObject doSomethingOnMainThread]に注目してもらいたい。-[NSObject performSelectorOnMainThread:withObject:waitUntilDone:]をNSProxyのサブクラス(MyProxy)に送っているがMyProxy ()カテゴリのおかげで”…may not respond to…”の警告が表示されない。

//
//  MyObject.m
//
 
#import "MyProxy.h"
 
//This is important!!
@interface MyProxy ()
-(void)doSomething;
@end
 
@implementation MyObject 
 
-(void)doSomethingOnMainThread{
	MyProxy *myProxy = [[[MyProxy alloc] initWithTarget:self] autorelease];
 
	//No warnings!!
	[myProxy doSomething];
}
 
-(void)doSomething{
	//Does something.
}
 
@end
//
//  MyProxy.h
//
 
#import <Foundation/Foundation.h>
 
@interface MyProxy : NSProxy {
	id _target;
}
 
-(id)initWithTarget:(id)aTarget;
 
@end
//
//  MyProxy.m
//
 
#import "MyProxy.h"
 
@implementation MyProxy
 
-(id)initWithTarget:(id)aTarget{
	_target = aTarget;
	return self;
}
 
//Override
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
	return [_target methodSignatureForSelector:aSelector];
}
 
//Override
-(void)forwardInvocation:(NSInvocation *)anInvocation{
	[anInvocation performSelectorOnMainThread:@selector(invokeWithTarget:)
		withObject:_target waitUntilDone:NO];
}
 
-(void)dealloc{
	_target = nil;
	[super dealloc];
}
 
@end

Reference

Footnotes

  1. メッセージフォワーディングの具体例は-[NSObject performSelector:withObject]に複数の引数を渡す – boreal-kiss.com []
Read More

画像をドロップするとiPhone/iPad用のアイコン用にリサイズするMac用アプリケーションです。対応サイズと出力ファイル名は以下の通り1

  • Icon.png – 57×57ピクセルのiPhoneアイコン
  • Icon@2x.png – 114×114ピクセルのiPhone 4アイコン
  • Icon-72.png – 72×72ピクセルのiPadアイコン
  • Icon-Small.png – 29×29ピクセルのiPhoneセッティングアイコン
  • Icon-Small@2x.png – 58×58ピクセルのiPhone 4セッティングアイコン
  • Icon-Small-50.png – 50×50ピクセルのiPadセッティングアイコン

ソースとアプリケーション本体は以下に置いてあります。

Footnotes

  1. Updating for the iPhone 4 retinal display – Blog – Use Your Loaf []
Read More

イントロダクション

Objective-Cのカテゴリはサブクラスを作ることなくメソッドの拡張(またはメソッドごとの分類)が行える点で強力だ。しかしメソッドの定義はできてもインスタンス変数の追加ができない。そこでCocoaのKey Value Coding (KVC)の機能を用いてカテゴリに擬似的にインスタンス変数を生成させる。これにより元クラスを手直しすることなくインスタンス変数を追加することができる。なおこの手法は、よりフレキシブルという理由でNSMapTableを用いるのが一般的なようだが、ここではNSMutableDictionaryを代用した。理由は使い慣れているからだ。NSMapTableを用いた方法はBuck and Yacktman – Cocoa Design Patternsが詳しい。

具体例

簡単な例としてボタンを押すとアラートを表示するアプリケーションを考える。ボタンプッシュ後の動作はAppControllerクラスで以下のように定義されているとする。

//AppController.m
 
@implementation AppController
 
-(IBAction)buttonPressed:(id)sender{
	NSRunAlertPanel(@"Alert", @"This is an alert.", @"OK", nil, nil);
}
 
@end

ここで「一度アラート表示された場合は再度アラートを表示させない」仕様に変更するとしよう。AppControllerにBOOL値を格納するインスタンス変数を追加して制御するのが手っ取り早いが、ここではAppControllerのカテゴリ(ShowAtOnce)を新規に定義して制御することにする。カテゴリ作成の大まかな手順は以下のようになる。

  1. 疑似インスタンス変数格納用にNSMutableDictionary変数をカテゴリに定義
  2. 疑似インスタンス変数としたいオブジェクトを上記NSMutableDictionary変数に格納。その際のキーストリングはAppControllerインスタンスに固有のものを用いる
  3. 格納したオブジェクトのAccesors (Setters/Getters)をカテゴリに定義
  4. インスタンスの解放時にNSMutableDictionaryからオブジェクトを消去するメソッドをカテゴリに定義

以下順にみていこう。

1. 疑似インスタンス変数格納用変数の定義

疑似インスタンス変数にしても情報を格納する場所は必要になる。そこで疑似インスタンス変数を格納する場所としてNSMutableDictionaryオブジェクトをカテゴリ(ShowAtOnce)に定義する。さらにこのオブジェクトのAccesorsをクラスメソッドとして定義する。つまりこの格納用のNSMutableDictionaryオブジェクトはAppControllerインスタンスすべてで使い回されることになる。

//AppController-ShowAtOnce.m
 
@implementation AppController (ShowAtOnce)
 
static NSMutableDictionary *_simulatedIVars = nil;
 
+(NSMutableDictionary *)_simulatedIVars{
	if (_simulatedIVars == nil){
		_simulatedIVars = [[NSMutableDictionary alloc] init];
	}
	return _simulatedIVars;
}
 
@end

2. インスタンス固有のキーストリング

上記例のアプリケーションの仕様を実現するためには、一度アラートが表示されたかどうかを調べるBOOL値を(NSMutableDictionary *)_simulatedIVarsに格納できればよさそうだ。しかし今は疑似インスタンス変数を作りたいのでAppControllerインスタンスごとに異なる値を格納できるようにする必要がある。そこでまず「AppControllerインスタンスに固有なNSStringオブジェクト」を生成するメソッドを定義する。このメソッドは-[AppController self]のストリング表現を返す。

//AppController-ShowAtOnce.m
 
-(NSString *)_instanceID{
	return [NSString stringWithFormat:@"%@", self];
}

さらに「疑似インスタンス変数名に相当するNSStringオブジェクト」を用意する。これはクラス全体で使い回しができるようにすればよい。ここではAlertHasEverBeenShownKeyStringという名前で定義しておこう。

//AppController-ShowAtOnce.m
 
static NSString * const AlertHasEverBeenShownKeyString = @"alertHasEverBeenShown";

あとは「AppControllerインスタンスに固有なNSStringオブジェクト」と「疑似インスタンス変数名に相当するNSStringオブジェクト」を適当に繋げて「あるAppControllerインスタンスの疑似インスタンス変数に固有なNSStringオブジェクト」を作成するメソッドを定義する。このメソッドの返り値が(NSMutableDictionary *)_simulatedIVarsへの格納用キーストリングとして利用されるわけだ。

//AppController-ShowAtOnce.m
 
-(NSString *)_objectKeyForKeyString:(NSString *)aKeyString{
	return [NSString stringWithFormat:@"%@:%@", 
		[self _instanceID], aKeyString];
}

現在までのAppController (ShowAtOnce)の中身をまとめると以下のようになっているはずだ。

//AppController-ShowAtOnce.m
 
static NSString * const AlertHasEverBeenShownKeyString = @"alertHasEverBeenShown";
 
@implementation AppController (ShowAtOnce)
 
static NSMutableDictionary *_simulatedIVars = nil;
 
+(NSMutableDictionary *)_simulatedIVars{
	if (_simulatedIVars == nil){
		_simulatedIVars = [[NSMutableDictionary alloc] init];
	}
	return _simulatedIVars;
}
 
#pragma mark -
#pragma mark Utilities
 
-(NSString *)_instanceID{
	return [NSString stringWithFormat:@"%@", self];
}
 
-(NSString *)_objectKeyForKeyString:(NSString *)aKeyString{
	return [NSString stringWithFormat:@"%@:%@", 
		[self _instanceID], aKeyString];
}
 
@end

3. 疑似インスタンス変数のAccesors

上記メソッド群を利用してカテゴリ(ShowAtOnce)に定義する。Settersは-[AppController _setAlertHasEverBeenShown:]、Gettersは-[AppController _alertHasEverBeenShown]としよう。ここはstraightforwardだと思う。

//AppController-ShowAtOnce.m
 
-(BOOL)_alertHasEverBeenShown{
	NSString *aKey = 
		[self _objectKeyForKeyString:AlertHasEverBeenShownKeyString];
	id object = [[[self class] _simulatedIVars] objectForKey:aKey];
	if (object == nil){
		return NO;
	}
 
	return [object boolValue];
}
 
-(void)_setAlertHasEverBeenShown:(BOOL)yn{
	NSString *aKey = 
		[self _objectKeyForKeyString:AlertHasEverBeenShownKeyString];
	[[[self class] _simulatedIVars] 
		setObject:[NSNumber numberWithBool:yn] forKey:aKey];
}

あとはAppController内でこのメソッドを使ってやればよい。具体的には以下のようになるだろう。

//AppController.m
 
#import "AppController-ShowAtOnce.h"
 
@implementation AppController
 
-(IBAction)buttonPressed:(id)sender{
	if (![self _alertHasEverBeenShown]){
		NSRunAlertPanel(@"Alert", @"This is an alert.", @"OK", nil, nil);
		[self _setAlertHasEverBeenShown:YES];
	}
}
 
@end

4. 疑似インスタンス変数を解放

AppControllerインスタンスごとに(NSMutableDictionary *)_simulatedIVarsに登録されるのだからAppControllerインスタンスが不要になったら疑似インスタンス変数も解放する必要がある。そのために以下のようなメソッドをAppController(ShowAtOnce)に定義しておく。このメソッドは呼ばれれば不要になった疑似インスタンス変数を解放する。

//AppController-ShowAtOnce.m
 
-(void)_removeSimulatedIVars{
	NSString *aKey = 
		[self _objectKeyForKeyString:AlertHasEverBeenShownKeyString];
	id object = [[[self class] _simulatedIVars] objectForKey:aKey];
 
	if (object){
		[[[self class] _simulatedIVars] removeObjectForKey:aKey];
	}
}

さてこれまでAppControllerそのものを一切変更しないように実装すると言ってきたが実は嘘で、AppControllerそのものを手直しする必要がでてくる。というのも疑似インスタンス変数の解放を呼ぶメソッドをカテゴリに書くことは危険だからだ。例えば-[AppController dealloc]をカテゴリ(ShowAtOnce)に再定義することは可能だが、もし他に疑似インスタンス変数を持つ別カテゴリが同様の-[AppController dealloc]を実装した場合どちらの-[AppController dealloc]が呼び出されるかわからなくなるからだ(結果どちらかのカテゴリの疑似インスタンス変数は解放メソッドを呼ばれないがために生き残ってしまう)。したがって疑似インスタンス変数の解放メソッドを呼び出すのはAppControllerとするのがよいだろう。

//AppController.m
 
-(void)dealloc{
	[self _removeSimulatedIVars];
	[super dealloc];
}

全ソースコード

最終的なコードは以下のようになっている。

//AppController.h
 
#import <Cocoa/Cocoa.h>
 
@interface AppController : NSObject {
 
}
 
-(IBAction)buttonPressed:(id)sender;
@end
//AppController.m
 
#import "AppController.h"
#import "AppController-ShowAtOnce.h"
 
@implementation AppController
 
-(IBAction)buttonPressed:(id)sender{
	if (![self _alertHasEverBeenShown]){
		NSRunAlertPanel(@"Alert", @"This is an alert.", @"OK", nil, nil);
		[self _setAlertHasEverBeenShown:YES];
	}
}
 
-(void)dealloc{
	[self _removeSimulatedIVars];
	[super dealloc];
}
 
@end
//AppController-ShowAtOnce.h
 
#import <Cocoa/Cocoa.h>
#import "AppController.h"
 
@interface AppController (ShowAtOnce)
 
+(NSMutableDictionary *)_simulatedIVars;
-(void)_removeSimulatedIVars;
-(BOOL)_alertHasEverBeenShown;
-(void)_setAlertHasEverBeenShown:(BOOL)yn;
-(NSString *)_instanceID;
-(NSString *)_objectKeyForKeyString:(NSString *)aKeyString;
@end
//AppController-ShowAtOnce.m
 
#import "AppController-ShowAtOnce.h"
 
//Private
static NSString * const AlertHasEverBeenShownKeyString = @"alertHasEverBeenShown";
 
@implementation AppController (ShowAtOnce)
 
static NSMutableDictionary *_simulatedIVars = nil;
 
+(NSMutableDictionary *)_simulatedIVars{
	if (_simulatedIVars == nil){
		_simulatedIVars = [[NSMutableDictionary alloc] init];
	}
	return _simulatedIVars;
}
 
-(void)_removeSimulatedIVars{
	NSLog(@"%s", __FUNCTION__);
 
	NSString *aKey = 
		[self _objectKeyForKeyString:AlertHasEverBeenShownKeyString];
	id object = [[[self class] _simulatedIVars] objectForKey:aKey];
 
	if (object){
		[[[self class] _simulatedIVars] removeObjectForKey:aKey];
	}
}
 
#pragma mark -
#pragma mark Setters Getters
 
-(BOOL)_alertHasEverBeenShown{
	NSString *aKey = 
		[self _objectKeyForKeyString:AlertHasEverBeenShownKeyString];
	id object = [[[self class] _simulatedIVars] objectForKey:aKey];
	if (object == nil){
		return NO;
	}
 
	return [object boolValue];
}
 
-(void)_setAlertHasEverBeenShown:(BOOL)yn{
	NSString *aKey = 
		[self _objectKeyForKeyString:AlertHasEverBeenShownKeyString];
	[[[self class] _simulatedIVars] 
		setObject:[NSNumber numberWithBool:yn] forKey:aKey];
}
 
#pragma mark -
#pragma mark Utilities
 
-(NSString *)_instanceID{
	return [NSString stringWithFormat:@"%@", self];
}
 
-(NSString *)_objectKeyForKeyString:(NSString *)aKeyString{
	return [NSString stringWithFormat:@"%@:%@", 
		[self _instanceID], aKeyString];
}
 
@end
Read More

イントロダクション

-[NSObject performSelector:withObject]に複数の引数を渡す – boreal-kiss.comで少しだけ言及していたHigher Order Messaging (HOM)の例をいくつか挙げておく。なおCocoaにおけるHOMはMetaobjectによるMPWFoundationの機能の一部として利用することができる(ようである)。

ケース1

ボタンを押す(-[AppController buttonPressed:])とアラートを表示する(-[AppController openAlert])アプリケーションを考えよう。アプリケーションのコントローラー部分は以下のように記述されているとする。

@implementation AppController
 
-(IBAction)buttonPressed:(id)sender{
	[self openAlert];
}
 
-(void)openAlert{
	NSRunAlertPanel(@"Alert", @"This is an alert", @"OK", nil, nil);
}
 
@end

1秒後にアラート表示をさせたい場合は-[NSObject performSelector:withObject:afterDelay:]を用いると以下のように書き換えることができる。

-(IBAction)buttonPressed:(id)sender{
	[self performSelector:@selector(openAlert) withObject:nil afterDelay:1.0];
}

この書き方でももちろんかまわないが、HOMを用いるとメッセージ内容がより直感的に表現される。新しくNSProxyのサブクラス(DelayedTrampoline)を導入してHOM的な書き方をした場合は以下のようになる。

#import "DelayedTrampoline.h"
 
@implementation AppController
 
//New
-(IBAction)buttonPressed:(id)sender{
	[[self afterDelay:1.0] openAlert];
}
 
-(void)openAlert{
	NSRunAlertPanel(@"Alert", @"This is an alert", @"OK", nil, nil);
}
 
//New
-(DelayedTrampoline *)afterDelay:(NSTimeInterval)delay{
	return [DelayedTrampoline delayedTrampolineWithTarget:self 
			afterDelay:delay];
}
 
@end
@interface DelayedTrampoline : NSProxy {
	id _target;
	NSTimeInterval _delay;
}
 
-(id)initWithTarget:(id)aTarget afterDelay:(NSTimeInterval)delay;
+(id)delayedTrampolineWithTarget:(id)aTarget afterDelay:(NSTimeInterval)delay;
@end
@implementation DelayedTrampoline
 
-(id)initWithTarget:(id)aTarget afterDelay:(NSTimeInterval)delay{
	_target = aTarget;
	_delay = delay;
	return self;
}
 
+(id)delayedTrampolineWithTarget:(id)aTarget afterDelay:(NSTimeInterval)delay{
	return [[[[self class] alloc] initWithTarget:aTarget 
				afterDelay:delay] autorelease];
}
 
//Override
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
	return [_target methodSignatureForSelector:aSelector];
}
 
//Override
-(void)forwardInvocation:(NSInvocation *)anInvocation{
	[anInvocation performSelector:@selector(invokeWithTarget:) 
		withObject:_target afterDelay:_delay];
}
 
-(void)dealloc{
	_target = nil;
	[super dealloc];
}
 
@end

以下に比較したい箇所だけを抜き出す。HOMの場合メッセージ(afterDelay:)の引数がメッセージ(openAlert)であるかのように書くことができることがわかる。これがHOMの”Higher Order”たる所以である。[NSObject performSelector:withObject:afterDelay:]を用いた従来の書き方よりもより直感的に理解できる表現だ。もちろん引数に書かれたメッセージは実際に引数を持つことができるため柔軟性も高い(例: -[NSObject performSelector:withObject]に複数の引数を渡す – boreal-kiss.com)。

//Normal
-(IBAction)buttonPressed:(id)sender{
	[self performSelector:@selector(openAlert) withObject:nil afterDelay:1.0];
}
 
//HOM
-(IBAction)buttonPressed:(id)sender{
	[[self afterDelay:1.0] openAlert];
}

ケース2

HOMは同じ内容を繰り返し記述しなければならない場合などにも便利だ。例えばdelegateメソッドを実行する際にターゲットであるdelegateが実際にメソッドを実装しているか確認する次のようなコードを大量に書いて(あるいはコピペして)骨が折れた経験はないだろうか。

if ([_delegate respondsToSelector:@selector(doSomething)]){
	[_delegate doSomething];
}

この場合あらかじめdelegateオブジェクトを扱うクラス(例えばAppController)にHOMを実装しておけば問題は回避可能だ。

@interface AppController : NSObject {
	id _delegate;
}
@end
#import "IfRespondsToTrampoline.h"
 
@implementation AppController
 
-(IfRespondsToTrampoline *)delegateIfRespondsTo{
	if (_delegate){
		return [IfRespondsToTrampoline 
			ifRespondsToTrampolineWithTarget:_delegate];
	}
	return nil;
}
 
@end
@interface IfRespondsToTrampoline : NSProxy {
	id _target;
}
 
-(id)initWithTarget:(id)aTarget;
+(id)ifRespondsToTrampolineWithTarget:(id)aTarget;
@end
@implementation IfRespondsToTrampoline
 
-(id)initWithTarget:(id)aTarget{
	_target = aTarget;
	return self;
}
 
+(id)ifRespondsToTrampolineWithTarget:(id)aTarget{
	return [[[[self class] alloc] initWithTarget:aTarget] autorelease];
}
 
//Override
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
	return [_target methodSignatureForSelector:aSelector];
}
 
//Override
-(void)forwardInvocation:(NSInvocation *)anInvocation{
	[anInvocation invokeWithTarget:_target];
}
 
-(void)dealloc{
	_target = nil;
	[super dealloc];
}
 
@end

以下に比較したい箇所だけ抜粋する。AppControllerに上記のようなHOMが実装されていれば、delegateオブジェクトがメソッドを実装していれば実行、なければ何もしない(実際には内部でNSProxyによるNSInvalidArgumentExceptionが挙がる)というコードが一行で書けてしまう。

//In AppController.m
 
//Normal
if ([_delegate respondsToSelector:@selector(doSomething)]){
	[_delegate doSomething];
}
 
//HOM
[[self delegateIfRespondsTo] doSomething];
Read More

イントロダクション

あるメソッドを遅延して実行したい場合、-[NSObject performSelector:withObject:afterDelay]を用いることが多々ある。しかし対象のメソッドが複数の引数を持つものであった場合、この方法は直接的には用いることができなくなってしまう。そこでNSInvocationクラスとNSProxyクラスを用いて複数引数を持つメソッドでも適用できるように工夫する。なおこの方法はHigher Order Messaging(メッセージを引数に持つメッセージという意味合い)と呼ばれる。

*******************************************************************
HOMについてまとめた記事はこちら。
Higher Order Messaging (HOM) – boreal-kiss.com
*******************************************************************

問題の一例

例えば以下のようなコントローラークラスを考える。このコントローラーではUI中のボタン(Show Alert)が押されるとアラートが表示される仕組みになっている。

@implementation AppController
 
-(IBAction)buttonPressed:(id)sender{
	[self openAlertWithTitle:@"Alert" message:@"This is an alert." 
		defaultButtonTitle:@"OK"];
}
 
-(void)openAlertWithTitle:(NSString *)aTitle 
				message:(NSString *)aMessage 
				defaultButtonTitle:(NSString *)aDefaultButtonTitle{
	NSRunAlertPanel(aTitle, aMessage, aDefaultButtonTitle, nil, nil);
}
 
@end

このままだと、ボタンが押された途端にアラートが表示され、ボタンの描画状態がプッシュ中のままになることがわかる。気持ちわるい。

そこで-[NSObject performSelector:withObject:afterDelay:]を用いてアラートを遅延表示させるのが得策だろう。しかし-[NSObject performSelector:withObject:afterDelay:]では-[AppController showAlertWithTitle:message:defaultButtonTitle:]を正しく実行できない。引数が複数あるためだ。それではどうすればよいだろうか。解決策のひとつとして複数の引数をひとつにまとめてしまう方法が考えられる。

NSInvocation

NSInvocationはメソッドをオブジェクト化したクラスである。デザインパターンで言うところのCommandパターンに相当するもので、これを利用することで上記問題を解決することができる。具体的には以下のような手順を踏むことになるだろう。

  1. -[NSObject performSelector:withObject:afterDelay:]で本来実行したい複数引数を持つメソッドをNSInvocationオブジェクトとしてひとつにまとめてしまう(メソッドのオブジェクト化)
  2. 作成したNSInvocationオブジェクトを実行するための新たなメソッド(引数はNSInvocationオブジェクトひとつのみ)を定義、それを-[NSObject performSelector:withObject:afterDelay:]で実行する。単一引数なので問題なし
  3. NSInvocationオブジェクトの中身(本来実行したいメソッド)を実行する

以下では新しく-[AppController openDelayedAlertWithTitle:message:defaultButtonTitle:]としてアラートの遅延表示を実現している。

@implementation AppController
 
-(IBAction)buttonPressed:(id)sender{
	[self openDelayedAlertWithTitle:@"Alert" message:@"This is an alert."
		defaultButtonTitle:@"OK"];
}
 
-(void)openAlertWithTitle:(NSString *)aTitle
	message:(NSString *)aMessage
	defaultButtonTitle:(NSString *)aDefaultButtonTitle{
	NSRunAlertPanel(aTitle, aMessage, aDefaultButtonTitle, nil, nil);
}
 
//New
-(void)openDelayedAlertWithTitle:(NSString *)aTitle
	message:(NSString *)aMessage
	defaultButtonTitle:(NSString *)aDefaultButtonTitle{
 
	NSMethodSignature *aSignature = [[self class] 
		instanceMethodSignatureForSelector:
		@selector(openAlertWithTitle:message:defaultButtonTitle:)];
	NSInvocation *anInvocation = [NSInvocation 
		invocationWithMethodSignature:aSignature];
	[anInvocation setTarget:self];
	[anInvocation setArgument:&aTitle atIndex:2];
	[anInvocation setArgument:&aMessage atIndex:3];
	[anInvocation setArgument:&aDefaultButtonTitle atIndex:4];
	[anInvocation setSelector:
		@selector(openAlertWithTitle:message:defaultButtonTitle:)];
	[self performSelector:@selector(performInvocation:)
		withObject:anInvocation afterDelay:0.0];
}
 
//New
-(void)performInvocation:(NSInvocation *)anInvocation{
	[anInvocation invokeWithTarget:self];
}
 
 
@end

実行結果。アラート表示が遅延されボタンがプッシュ状態でなくなっているのがわかる。気持ちいい。

上記例からメソッドをオブジェクト化するというNSInvocationクラスの有用性が実感できた。反面、NSInvocationオブジェクトは作成するまでの手順が面倒なこともわかったと思う(例えば引数が10個あるメソッドをNSInvocationオブジェクト化するためには10回も-[NSInvocation setArgument:atIndex:]を書かないといけない)。実は適当なメッセージを送るとメッセージ内容をNSInvocationオブジェクトにしてくれる便利クラスが存在する。NSProxyだ。

NSProxy

NSProxyはデザインパターンで言うところのProxyパターンを実現するものである。NSProxyは自分が理解できないメッセージをとりあえずNSInvocationオブジェクトにしてしまうという機能を持っているのでこれを利用する。NSProxyはメッセージを受け取ると以下のような手順でNSInvocationオブジェクトを作成し、NSInvocationオブジェクトをどのように扱うのか指示を待つ。

  1. -[NSProxy methodSignatureForSelector:]を呼び出しNSMthodSignatureオブジェクトを作成する。ただしNSMethodSignatureオブジェクトを作成するための必要な情報(ターゲット etc.)はメソッド内に適切に与えてやる必要がある
  2. NSMethodSignatureオブジェクトを元にNSInvocationオブジェクトを作成する
  3. -[NSProxy forwardInvocation:]を呼び出しメソッド内容の指示に従う

自動作成されたNSInvocationオブジェクトは-[NSProxy forwardInvocation:]の引数に渡されるので、あとはこのNSInvocationオブジェクトの扱い方を内部に記述してやればよい。以下ではNSProxyのサブクラスとしてTrampolineクラスを用いてNSInvocationオブジェクトを作成させている。AppControllerの中身の変更にも注目してもらいたい。

#import "Trampoline.h"
 
@implementation AppController
 
-(IBAction)buttonPressed:(id)sender{
	[self openDelayedAlertWithTitle:@"Alert" message:@"This is an alert."
		defaultButtonTitle:@"OK"];
}
 
-(void)openAlertWithTitle:(NSString *)aTitle
	message:(NSString *)aMessage
	defaultButtonTitle:(NSString *)aDefaultButtonTitle{
	NSRunAlertPanel(aTitle, aMessage, aDefaultButtonTitle, nil, nil);
}
 
//New
-(void)openDelayedAlertWithTitle:(NSString *)aTitle
	message:(NSString *)aMessage
	defaultButtonTitle:(NSString *)aDefaultButtonTitle{
	[[self trampoline] openAlertWithTitle:aTitle
		message:aMessage defaultButtonTitle:aDefaultButtonTitle];
}
 
-(void)performInvocation:(NSInvocation *)anInvocation{
	[anInvocation invokeWithTarget:self];
}
 
//New
-(Trampoline *)trampoline{
	return [Trampoline trampolineWithTarget:self
		selector:@selector(performInvocation:)];
}
 
 
 
@end
@interface Trampoline : NSProxy {
	id _target;
	SEL _selector;
}
 
-(id)initWithtarget:(id)aTarget selector:(SEL)aSelector;
+(id)trampolineWithTarget:(id)aTarget selector:(SEL)aSelector;
 
@end
@implementation Trampoline
 
-(id)initWithtarget:(id)aTarget selector:(SEL)aSelector{
	_target = aTarget;
	_selector = aSelector;
	return self;
}
 
+(id)trampolineWithTarget:(id)aTarget selector:(SEL)aSelector{
	return [[[[self class] alloc] initWithtarget:aTarget selector:aSelector] 
		autorelease];
}
 
//Override
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
	return [_target methodSignatureForSelector:aSelector];
}
 
//Override
-(void)forwardInvocation:(NSInvocation *)anInvocation{
	[_target performSelector:_selector withObject:anInvocation afterDelay:0.0];
}
 
-(void)dealloc{
	_target = nil;
	[super dealloc];
}
 
@end

References

Read More

まとめ

  • -[NSObject performSelector:]の返り値として不適切なものが存在する。
  • それら不適切なものを扱う場合の対処法について。

イントロダクション

NSObjectのインスタンスメソッド

- (void)performSelector:(SEL) aSelector

はvoid型となっているが、「引数にセットされたメソッドの返り値」を自分の返り値に持つことができる。ただしその場合、-[NSObject performSelector:]の返り値はid型(オブジェクトポインタ)になるので適切にキャストしてやる必要がある。例えば、以下のようなNSStringを返すメソッドを持つTest1クラスを考える。

@implementation Test1
 
-(NSString *)stringValue{
	return @"Hello";
}
 
@end

このTest1クラスに-[NSObject performSelector:]を使って-[Test1 stringValue]を呼び、NSStringにキャストして返り値を受け取るとたしかに@”Hello”が返ってきていることがわかる。

Test1 *test1 = [[Test1 alloc] init];
NSString *stringValue = (NSString *)[test1 performSelector:@selector(stringValue)];
NSLog(@"%@", stringValue);// Hello

本題

-[NSObject performSelector:]の返り値はid型であり、id型はオブジェクトへのポインタであると説明した。つまり、-[NSObject performSelector:]は「オブジェクトではないものを返り値とするメソッド」の返り値を受け取ることができない可能性があることがわかる。例を見てみよう。Test2クラスに以下のようなメソッドがあるとする。どのメソッドの返り値もオブジェクトではない。

@implementation Test2
 
-(BOOL)boolValue{
	return YES;
}
 
-(int)intValue{
	return 10;
}
 
-(float)floatValue{
	return 2.0;
}
 
-(CGPoint)point{
	return CGPointMake(100.0, 100.0);
}
 
@end

先ほどと同様にこれらのメソッドを-[NSObject performSelector:]を使って呼び出し、返り値を確認すると以下のような結果になった。

Test2 *test2 = [[Test2 alloc] init];
 
//warning: cast from pointer to integer of different size
BOOL boolValue = (BOOL)[test2 performSelector:@selector(boolValue)];
NSLog(@"%i", boolValue);// 1
 
int intValue = (int)[test2 performSelector:@selector(intValue)];
NSLog(@"%i", intValue);// 10
 
//error: pointer value used where a floating point value was expected
float floatValue = (float)[test2 performSelector:@selector(floatValue)];
NSLog(@"%f", floatValue);
 
//error: conversion to non-scalar type requested
CGPoint point = (CGPoint)[test2 performSelector:@selector(point)];
NSLog(@"%@", NSStringFromCGPoint(point));

結果をまとめると以下のようになる。

  • 返り値BOOLについては警告が出たが返り値の受け取りができた。
  • 返り値intについては警告は出ず返り値の受け取りができた。
  • 返り値floatについてはコンパイルエラーが生じ実行できなかった。
  • 返り値CGPointについてはコンパイルエラーが生じ実行できなかった。

対処法

上記のテスト結果から、-[NSObject performSelector:]から「オブジェクトではないものを返り値とするメソッド」の返り値を受け取るためには何かしらの工夫をしなければならないことがわかった(int型・BOOL型などの例外もある)。以下に二種類の対処法を挙げておく。

  1. -[NSObject performSelector:]で呼び出すメソッドの返り値をオブジェクトにしておく。
  2. -[NSObject performSelector:]の代わりに[NSInvocation invoke]を用いる。

順に見てみよう。

対処法1

二つ目の対処法より簡単である。返り値int, BOOL, floatについてはNSNumberオブジェクトによるラッピング、返り値CGPoint(CGRect, CGSizeなども含む)についてはNSStringオブジェクトに変換することで簡単に対処できる。以下に、Test2クラスの4メソッドの返り値を全てオブジェクトに変更したTest3クラスを挙げる。

@implementation Test3
 
-(NSNumber *)boolValue{
	return [NSNumber numberWithBool:YES];
}
 
-(NSNumber *)intValue{
	return [NSNumber numberWithInt:10];
}
 
-(NSNumber *)floatValue{
	return [NSNumber numberWithFloat:2.0];
}
 
-(NSString *)point{
	return NSStringFromCGPoint(CGPointMake(100.0, 100.0));
}
 
@end

この場合、各メソッドの返り値はオブジェクトであるため、それらオブジェクトから必要な値を復元してやればよい。

Test3 *test3 = [[Test3 alloc] init];
 
BOOL boolValue = [[test3 performSelector:@selector(boolValue)] boolValue];
NSLog(@"%i", boolValue2);// 1
 
int intValue = [[test3 performSelector:@selector(intValue)] intValue];
NSLog(@"%i", intValue2);// 10
 
float floatValue = [[test3 performSelector:@selector(floatValue)] floatValue];
NSLog(@"%f", floatValue2);// 2.000000
 
CGPoint point = CGPointFromString([test3 performSelector:@selector(point)]);
NSLog(@"%@", NSStringFromCGPoint(point2));// {100, 100}

対処法2

-[NSObject performSelector:]の代わりにNSInvocationを用いると、返り値がオブジェクトであろうとなかろうと受け取ることが可能になる。例えば、-[Test2 floatValue]の返り値を受け取る場合には、以下のような手続きをとる。

Test2 *test2 = [[Test2 alloc] init];
 
float floatValue;
NSMethodSignature *aSigniture 
        = [[test2 class] instanceMethodSignatureForSelector:@selector(floatValue)];
NSInvocation *anInvocation 
        = [NSInvocation invocationWithMethodSignature:aSigniture];
[anInvocation setSelector:@selector(floatValue)];
[anInvocation setTarget:test2];
[anInvocation invoke];
[anInvocation getReturnValue:&floatValue];
NSLog(@"%f", floatValue);// 2.000000

NSInvocationを用いると汎用性は高くなるが、一般的にソースが煩雑になる。

参考

Read More

[iPhone] HTMLリンクのようなUIButtonのサブクラスでボタンタイトルの文字列の長さに応じてアンダーラインを引く方法を試行錯誤していたが、NSStringオブジェクトのCGSizeを返すメソッドであっさり解決した。

//  UIStringDrawing.h
//  UIKit
 
@interface NSString(UIStringDrawing)
 
- (CGSize)sizeWithFont:(UIFont *)font;
 
@end

修正前

//  HTMLLinkButton.m
 
- (void)drawRect:(CGRect)rect {
	CGFloat w = rect.size.width;
	CGFloat h = rect.size.height;
	CGFloat capHeight = self.font.capHeight;
	CGFloat xHeight = self.font.xHeight;
	CGFloat underlineLength = [_buttonTitle length] * xHeight;
	CGFloat startX = (w - underlineLength) / 2.0;
	CGFloat startY = (h + capHeight) / 2.0;
	CGFloat endX = startX + underlineLength;
	CGFloat endY = startY;
 
	CGContextRef context = UIGraphicsGetCurrentContext();
	CGContextSetStrokeColorWithColor(context, 
		_colorController.currentColor.CGColor);
	CGContextMoveToPoint(context, startX, startY);
	CGContextAddLineToPoint(context, endX, endY);
	CGContextStrokePath(context);
 
}

修正後

//  HTMLLinkButton.m
 
- (void)drawRect:(CGRect)rect {
	CGFloat w = rect.size.width;
	CGFloat h = rect.size.height;
	CGFloat titleWidth = [_buttonTitle sizeWithFont:self.font].width;
	CGFloat titleHeight = [_buttonTitle sizeWithFont:self.font].height;
	CGFloat descender = self.font.descender;
	CGFloat startX = (w - titleWidth) / 2.0;
	CGFloat startY = (h + titleHeight) / 2.0 + descender;
	CGFloat endX = startX + titleWidth;
	CGFloat endY = startY;
 
	CGContextRef context = UIGraphicsGetCurrentContext();
	CGContextSetStrokeColorWithColor(context, 
		_colorController.currentColor.CGColor);
	CGContextMoveToPoint(context, startX, startY);
	CGContextAddLineToPoint(context, endX, endY);
	CGContextStrokePath(context);
}
Read More

UIKitにはテキストにアンダーラインを付ける仕組みがない。そのため、HTMLのリンク表示のようなボタンを作るためにはアンダーラインを自前で描画してやらなければならない。ここで問題になるのが、アンダーラインの長さと描画位置が、アンダーラインを付加したい文字列の長さとフォントサイズに依存するということだ。以下のカスタムクラス(HTMLLinkButton)は、ボタンのタイトル文字列の長さとフォントサイズに比例してアンダーラインを可能な限り適切な位置に描画する。

使い方は簡単で、ボタンタイトルとリンク先URLを設定してやるだけ。例えば、UIViewControllerクラスに以下のように記述してやると上記画像のようなHTMLリンクボタンが表示される。リンクをクリックするとテキストがハイライトされ、指定のURLへナビゲートされる。

#import "HTMLLinkButton.h"
 
- (void)viewDidLoad {
    HTMLLinkButton *button1 = [[HTMLLinkButton alloc] 
		initWithTitle:@"Google" url:@"http://google.com/"];
	button1.frame = CGRectMake(50, 100, 100, 37);
	button1.font = [UIFont systemFontOfSize:24];
	[self.view addSubview:button1];
	[button1 release];
 
	HTMLLinkButton *button2 = [[HTMLLinkButton alloc] 
		initWithTitle:@"Yahoo" url:@"http://yahoo.com/"];
	button2.frame = CGRectMake(150, 100, 150, 37);
	button2.font = [UIFont systemFontOfSize:40];
	[self.view addSubview:button2];
	[button2 release];
}

先ほど、可能な限り適切な位置に描画する、と言ったのには訳がある。ボタンタイトルの文字列の扱いに以下のような仮定があるからである。

  • ボタン文字列の高さはUIButton.font.capHeight(現在使用されているフォントの大文字の高さ)に等しい
  • ボタン文字列の各文字の幅はUIButton.font.xHeight(現在使用されているフォントの小文字xの高さ。幅ではない)に等しい

これらの仮定を用いた処理はHTMLButtonLinkクラスのdrawRect:メソッドで行われている。改良の余地アリ。

(**追記2009/05/02: 文字列の長さ・高さの扱いは– (CGSize)sizeWithFont:(UIFont *)fontでスマートに解決した)

Source code

//  HTMLLinkButton.h
 
#import <UIKit/UIKit.h>
 
extern CGFloat const HTMLLinkButtonDefaultButtonWidth;
extern CGFloat const HTMLLinkButtonDefaultButtonHeight;
extern NSUInteger const HTMLLinkButtonDefaulltButtonNormalColor;
extern NSUInteger const HTMLLinkButtonDefaulltButtonHighlightedColor;
 
@interface HTMLLinkButton : UIButton {
	NSString *_buttonTitle;
	NSString *_url;
	UIColor *_normalColor;
	UIColor *_highlightedColor;
	UIColor *_currentColor;
}
@property (nonatomic, retain) NSString *buttonTitle;
@property (nonatomic, retain) NSString *url;
@property (nonatomic, retain) UIColor *normalColor;
@property (nonatomic, retain) UIColor *highlightedColor;
 
/**
 * The designated initializer.
 * @param title	Button title.
 * @param url URL string to link to.
 * @param normalColor Color of the button title in the normal state.
 * @param highlightedColor Color of the button title when highlighted.
 */
-(id)initWithTitle:(NSString *)title url:(NSString *)url 
	normalColor:(UIColor *)normalColor 
	highlightedColor:(UIColor *)highlightedColor;
 
/**
 * The same as the above initialize 
 * except for that the title color sets to default.
 * @param title	Button title.
 * @param url URL string to link to.
 */
-(id)initWithTitle:(NSString *)title url:(NSString *)url;
@end
 
//  HTMLLinkButton.m
 
#import "HTMLLinkButton.h"
#import "HTMLLinkButton-Private.h"
#import "UIColor-HexEncoding.h"
 
CGFloat const HTMLLinkButtonDefaultButtonWidth = 72.0;
CGFloat const HTMLLinkButtonDefaultButtonHeight = 37.0;
NSUInteger const HTMLLinkButtonDefaulltButtonNormalColor = 0x2255AA;
NSUInteger const HTMLLinkButtonDefaulltButtonHighlightedColor = 0xFF9900;
 
@implementation HTMLLinkButton
@synthesize buttonTitle = _buttonTitle;
@synthesize normalColor = _normalColor;
@synthesize highlightedColor = _highlightedColor;
@synthesize url = _url;
 
-(id)initWithTitle:(NSString *)title url:(NSString *)url 
			normalColor:(UIColor *)normalColor 
			highlightedColor:(UIColor *)highlightedColor{
	if (self = [super init]){
		self.buttonTitle = title;
		self.url = url;
		self.normalColor = normalColor;
		self.highlightedColor = highlightedColor;
		_currentColor = _normalColor;
		[self setup];
	}
	return self;
}
 
-(id)initWithTitle:(NSString *)title url:(NSString *)url{
	UIColor *normalColor = 
		[UIColor colorFromHex:HTMLLinkButtonDefaulltButtonNormalColor];
	UIColor *highlightedColor = 
		[UIColor colorFromHex:HTMLLinkButtonDefaulltButtonHighlightedColor];
	return [self initWithTitle:title 
			url:url normalColor:normalColor 
			highlightedColor:highlightedColor];
}
 
//Override
- (void)drawRect:(CGRect)rect {
	CGFloat w = rect.size.width;
	CGFloat h = rect.size.height;
	CGFloat capHeight = self.font.capHeight;
	CGFloat xHeight = self.font.xHeight;
	CGFloat underlineLength = [_buttonTitle length] * xHeight;
	CGFloat startX = (w - underlineLength) / 2.0;
	CGFloat startY = (h + capHeight) / 2.0;
	CGFloat endX = startX + underlineLength;
	CGFloat endY = startY;
 
	CGContextRef context = UIGraphicsGetCurrentContext();
	CGContextSetStrokeColorWithColor(context, _currentColor.CGColor);
	CGContextMoveToPoint(context, startX, startY);
	CGContextAddLineToPoint(context, endX, endY);
	CGContextStrokePath(context);
}
 
- (void)dealloc {
	[_buttonTitle release];
	[_url release];
	[_normalColor release];
	[_highlightedColor release];
	[_currentColor release];
    [super dealloc];
}
 
#pragma mark -
#pragma mark Setters
 
//Override
-(void)setFrame:(CGRect)frame{
	[super setFrame:frame];
	[self setNeedsDisplay];
}
 
//Override
-(void)setFont:(UIFont *)font{
	[super setFont:font];
	[self setNeedsDisplay];
}
 
//Override
-(void)setNormalColor:(UIColor *)color{
	if (_normalColor != color){
		[_normalColor release];
		_normalColor = [color retain];
		[self setNeedsDisplay];
	}
}
 
//Override
-(void)setHighlightedColor:(UIColor *)color{
	if (_highlightedColor != color){
		[_highlightedColor release];
		_highlightedColor = [color retain];
		[self setNeedsDisplay];
	}
}
@end
 
//  HTMLLinkButton-Private.h
 
#import <UIKit/UIKit.h>
#import "HTMLLinkButton.h"
 
@interface HTMLLinkButton (Private)
 
/**
 * @private
 */ 
-(void)setup;
-(void)changeTextColor;
-(void)touchUpInside;
@end
 
//  HTMLLinkButton-Private.m
 
#import "HTMLLinkButton-Private.h"
 
@implementation HTMLLinkButton (Private)
 
-(void)setup{
	[self setFrame:CGRectMake(0, 0, HTMLLinkButtonDefaultButtonWidth, 
		HTMLLinkButtonDefaultButtonHeight)];
	[self setTitle:_buttonTitle forState:UIControlStateNormal];
	[self setTitle:_buttonTitle forState:UIControlStateHighlighted];
	[self setTitleColor:_normalColor forState:UIControlStateNormal];
	[self setTitleColor:_highlightedColor forState:UIControlStateHighlighted];
	[self addTarget:self action:@selector(changeTextColor) 
		forControlEvents:UIControlEventTouchDown];
	[self addTarget:self action:@selector(changeTextColor) 
		forControlEvents:UIControlEventTouchDragExit];
	[self addTarget:self action:@selector(changeTextColor) 
		forControlEvents:UIControlEventTouchDragEnter];
	[self addTarget:self action:@selector(touchUpInside) 
		forControlEvents:UIControlEventTouchUpInside];
}
 
-(void)changeTextColor{
	if (_currentColor == _normalColor){
		_currentColor = _highlightedColor;
	}
	else{
		_currentColor = _normalColor;
	}
	[self setNeedsDisplay];
}
 
-(void)touchUpInside{
	[self changeTextColor];
	[[UIApplication sharedApplication] openURL:[NSURL URLWithString:_url]];
}
 
@end
 
// UIColor-HexEncoding.h
 
#import <UIKit/UIKit.h>
 
@interface UIColor (HexEncoding)
+(UIColor *)colorFromHex:(NSUInteger)color24;
@end
 
// UIColor-HexEncoding.m
 
#import "UIColor-HexEncoding.h"
 
@implementation UIColor (HexEncoding)
 
+(UIColor *)colorFromHex:(NSUInteger)color24{
	CGFloat r = (color24 >> 16) / 255.0f;
	CGFloat g = (color24 >> 8 & 0xFF) / 255.0f;
	CGFloat b = (color24 & 0xFF) / 255.0f;
	return [UIColor colorWithRed:r green:g blue:b alpha:1.0f];
}
 
@end
 
Read More