[iPhone] HTMLリンクのようなUIButtonのサブクラス
Tags: iPhone, Objective-C, UIKit
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 = 0×2255AA; 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
[iPhone]スタンフォード大学iPhone app講義のpodcast第5回
第5回講義(2009/04/15、要iTunes)聴講。UIViewクラス(Flashで言うところのDisplayObject)について、クラス説明、描画方法(Core Graphics)、ビルトインアニメーションについてなど濃い内容だった。後半20分ほどライブコーディングもありおもしろかった。
- UIView.frameとUIView.boundsの違いについて図でわかりやすく説明。
- drawRect:を他から直接呼ぶべきでない理由を説明していたが、あまり納得できず。ちなみにdeallocを直接呼ぶべきでない理由も僕は理解していない。なんでそんなメソッドがパブリック状態で野放しなのかね。パブリックと言えば、Objective-Cのメソッドはアクセス制御不能で基本的に全部パブリック(プロパティは制御可能)。隠蔽したい場合はカテゴリを使ったりして一工夫しないといけない(例えばPrivate Methods)。
- UIView.centerというプロパティが存在した!聴講してなかったら一生気付かなかったかもしれない。
- UIViewのビルトインアニメーションについて結構な時間を割いて説明していた。食わず嫌いをしていたので知るよい機会になった。全てUIViewのクラスメソッドで設定できるので、よく使うアニメーション効果などはカテゴリで作っておくと便利かもしれない(例えばUIViewのフェードイン/フェードアウト)。
- マウス追従するUIViewアニメーションのライブコーディング。名前のクソ長いdelegateメソッドなどはドキュメントからコピーペーストして使っていた。やはり覚えられるもんじゃないんだな。スラスラ完成させて拍手で講義終了。
[iPhone]スタンフォード大学iPhone app講義のpodcast第4回
第4回講義(2009/04/13、要iTunes)聴講。第3回に引き続きUIKit(Cocoaフレームワーク)の説明。Delegate、MVC、Nibファイルについてなどフレームワークの骨格に関する内容だった。正直あんまり感想がない。
- CocoaのMVCの構造はいわゆるデザインパターンのMVCと構造が異なる(ModelとViewに依存関係がない)。MVCパターンは、「MとVとCの依存関係をできるだけ対等になくしてやる」という思想の上に成り立っていると考えると、Cocoa型MVC(Controller司令塔型)は異端児と言える。といっても僕も少し前までCocoa型MVCをMVCパターンだと思っていたが(MVCオルタナティブ - boreal-kiss.com)。
- 後半はNibファイルの話だがわかりにくかった。
- 放送用のためか、生徒の質問(マイク無し)を先生が繰り返すし発言するようになっていた。ラジオリスナー側には今まで聞こえなかったのでこれは非常に助かる。
UIPickerViewのクリック音を無効にする
Tags: iPhone, Objective-C, UIKit
ピッカーを回転させたときのカチカチというクリック音を無効にするにはUIPickerViewのsetSoundsEnabled:メソッドで引数をNOに設定してやればよい。しかしメソッド自体は定義されているのに、UIKit.h(UIPikerView.h)で宣言されていないらしく、そのまま使うとコンパイル時に警告がでる。警告は出るが呼び出しは可能なので気にならない人はそれでOK。気になる人は新たに宣言を追加してやればよい。UIKit.hやUIPickerView.hに直接書き込むのは怖いのでカテゴリを用意する(宣言だけ必要なのでヘッダーファイルのみ作成)。
// UIPickerView-Mute.h #import <UIKit/UIKit.h> @interface UIPickerView (Mute) -(void) setSoundsEnabled:(BOOL)enabled; @end
あとはUIPickerViewを呼び出すファイル内にインクルードしてやれば先ほどの警告が出なくなる。
#import "UIPickerView-Mute.h" UIPickerView *picker = [[UIPickerView alloc] init]; [picker setSoundsEnabled:NO];
[iPhone]スタンフォード大学iPhone app講義のpodcast第3回
第3回講義(2009/04/08、要iTunes)聴講。丸々一時間メモリ管理に関する話で有益だった。カスタムクラスを作成しながらretainカウント方式とプロパティ属性(setter/getter)の説明などが行われる。ライブコーディングなし。講義で特に良いと思ったのは以下。
- autoreleaseの仕組みについての詳細な説明。iPhone関連書籍は数あれど、実際どのタイミングでautoreleaseが機能しているのかまで言及しているものは少ない(Aaron Hillegass本に書かれているがこれはCocoa専門書)。その場しのぎで作成したオブジェクト以外にautoreleaseを使うと危険なことがわかった。
- プロパティ属性(@propertyと@synthesize)を使うことで実際に書く手間が省けたコード内容の説明。こちらもiPhone関連本には詳細は載っていない場合が多い(Appleのドキュメントには書かれている)。一番知りたかったatomic, nonatomicについては講義では触れていなかった。
おまけ
retainカウント方式はコードが煩雑になりやすいのでちょっとした書き忘れなどが原因で無駄なメモリを使わせることが多々ある(iPhoneはメモリ使用量がシビアなのでこれが致命傷になることも)。しかもどこでrelease忘れが生じているか中々見つからず無駄な時間ばかりとられることになる。そういうときはLLVM/Clang Static Analyzerがおすすめ。ビルド内容(?)を読み取って、ソースコードの問題点(release忘れ箇所)を見やすいhtmlで出力してくれる。コマンドラインによる動作が障壁になるかもしれないが小難しい操作ではない。実際の使用状況はclangでObjective-Cプログラムを静的解析 - きりかリポーツを参照ください。
