目的
UIControlを継承しターゲットアクションメカニズムを持つクラスを作る方法を理解する。

主要クラス
UIControl,CALayer

使用テンプレートプロジェクト
Window-based Application

プロジェクトの名称
aquabutton

事前に体験しておくべきドリル

サンプル実装説明


 このドリルではUIControlを継承し、描画に「CALayerのcontentsを理解する」で使ったCAGradientLayerを使う事でAquaライクなボタンを作成する。
 まずはプロジェクトaquabuttonを作成し、UIControlを継承したAquaButtonクラスを作成する。
 作成時はUIViewを継承クラスとして選び、AquaButton.hができた時点でUIViewをUIControlに書き換える。CAGradientLayerのインスタンスはハイライト状態、ディスエーブル状態などでグラデーションを変化させるので、internalButtonという名称でインスタンス変数として持つ。
 プロパティtextLabelは「CALayerのcontentsを理解する」を参照。その他にボタンの色相を指定できるようにhueプロパティを用意した。
#import
@class CAGradientLayer;

@interface AquaButton : UIControl {
CAGradientLayer* internalButton;
}
@property (readonly) UILabel* textLabel;
@property (assign) float hue;
@end

 今回、ボタンの状態ごとにグラデーションを作りなおせるよう、グラデーションカラー配列を作る部分をメソッドとして独立させた。内容自体は「CALayerのcontentsを理解する」と同じ。
- (NSArray*)gradientArrayWithHue:(float)hue_
saturation:(float)saturation
brightness:(float)brightness
{
CGColorRef halationTop = [UIColor colorWithHue:hue_
saturation:saturation * 0.2
brightness:brightness
alpha:1].CGColor;
CGColorRef halationBottom = [UIColor colorWithHue:hue_
saturation:saturation * 0.8
brightness:brightness * 0.8
alpha:1].CGColor;
CGColorRef normalTop = [UIColor colorWithHue:hue_
saturation:saturation
brightness:brightness * 0.8
alpha:1].CGColor;
CGColorRef normalBottom = [UIColor colorWithHue:hue_
saturation:saturation
brightness:brightness
alpha:1].CGColor;
NSMutableArray *colors = [NSArray arrayWithObjects:
(id)halationTop,
(id)halationBottom,
(id)normalTop,
(id)normalBottom, nil];
return colors;
}

 initWithFrame:メソッドも、グラデーション色配列をgradientArrayWithHue:saturation:brightness:メソッドで作るようにした以外は特に違いはない。

@implementation AquaButton
@synthesize textLabel, hue;
 ・
 ・
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
internalButton = [CAGradientLayer layer];
internalButton.frame = self.bounds;
internalButton.borderWidth = 1;
internalButton.cornerRadius = self.bounds.size.height / 4.0;
NSMutableArray *locations = [NSArray arrayWithObjects:
[NSNumber numberWithFloat:0.0],
[NSNumber numberWithFloat:0.5],
[NSNumber numberWithFloat:0.5],
[NSNumber numberWithFloat:1.0],nil];
[internalButton setLocations:locations];
[internalButton setColors:[self gradientArrayWithHue:hue saturation:1 brightness:1]];
[self.layer addSublayer:internalButton];
textLabel = [[[UILabel alloc] initWithFrame:self.bounds] autorelease];
textLabel.font = [UIFont boldSystemFontOfSize:18];
textLabel.textColor = [UIColor whiteColor];
textLabel.textAlignment = UITextAlignmentCenter;
textLabel.shadowColor = [UIColor grayColor];
textLabel.backgroundColor = [UIColor clearColor];
[self addSubview:textLabel];
}
return self;
}

 あとはsetEnabled:、setHighlighted:メソッドをオーバーライドすればUIButtonと同じ動きをするAqua風ボタンが完成する。
- (void)setEnabled:(BOOL)enabled
{
[super setEnabled:enabled];
[internalButton setColors:[self gradientArrayWithHue:hue
saturation:enabled?1:0.3
brightness:1]];
internalButton.borderColor = enabled
?([UIColor blackColor].CGColor):([UIColor lightGrayColor].CGColor);
}

- (void)setHighlighted:(BOOL)highlighted
{
[super setHighlighted:highlighted];
[internalButton setColors:[self gradientArrayWithHue:hue
saturation:1
brightness:highlighted?0.7:1]];
}

 あっけないが、ボタン程度ならこれで十分機能する。
 aquabuttonAppDelegateで今回作成したAquaButton、UIButtonをそれぞれwindowに配置してUIControlEventTouchDown、UIControlEventTouchDragInside、…、UIControlEventTouchUpInsideなどのイベントに対しターゲットアクションを設定し比べてみる。

$テン*シー*シー-1

 コンソールにはAquaButton、UIButtonどちらも同じように、指(カーソル)の動きに合わせてアクションメソッドが実行されていく。
EventTouchDown
UIControlEventTouchDragInside
UIControlEventTouchDragInside
UIControlEventTouchDragInside
UIControlEventTouchDragInside
UIControlEventTouchDragInside
UIControlEventTouchDragInside
UIControlEventTouchDragInside
UIControlEventTouchUpInside


 以上、AquaButtonはボタンとしてはUIButton同等の働きができる事がわかる。

$テン*シー*シー-2

 前回の応用としてUIViewを継承しCAGradientLayerを使ったBadgeViewもサンプルソースに含めておく。shadowOffset、shadowOpacityプロパティを使っている。
 こちらは、いわゆるバッジ。

$テン*シー*シー-3

プロジェクト


検討

 contentsにCGImageRefを設定する場合、retinaディスプレイに最適化したい場合、作成するCGContextRefを UIGraphicsBeginImageContext ではなくUIGraphicsBeginImageContextWithOptions を使って作成したり、CALayerのcontentsScaleプロパティを2.0にしたりを自分で調整しなければならない。
 retinaディスプレイかどうかは[[UIScreen mainScreen].scaleで調べる。
 iOS 4.0より前のiOSでも動かす予定なら、scaleプロパティを使う前に
  [UIScreen instancesRespondToSelector:@selector(scale)]
 といったチェックも必要。

 CAGradientLayerはその点においても使う価値がある。