ラベル UI の投稿を表示しています。 すべての投稿を表示
ラベル UI の投稿を表示しています。 すべての投稿を表示

2011年11月19日土曜日

UIWebView.scrollView に対して KVO を使うと色々面白い

iOS 5より、UIWebViewにscrollViewプロパティが追加され、たとえばスクロールを無効にしたりステータスバーをタップしても一番上に戻らないようにしたりなど、UIWebViewのスクロール周りの処理を外から自由に触れるようになりました。ですが便利なのはこれだけではありません。KVOの仕組みを使うことで、さらにUIWebViewを便利に使うことができます。ここでは私が使っている中で一番のおすすめをご紹介します。

■UIWebViewの描画しているHTMLのcontentSizeを非同期的に、リアルタイムで取得する
UIWebview.scrollViewのcontentSizeプロパティは、UIWebViewの描画しているHTMLの大きさ(contentSize)と同じ値になります。この性質を利用して、contentSizeプロパティにKVOを貼ると、UIWebViewの描画しているHTMLの大きさ(contentSize)が変わったタイミングで通知を受け取ることができます。こんなコードになります。
- (void)viewWillAppear:(BOOL)animated
{
  [self.webView.scrollView addObserver:self forKeyPath:@"contentSize" options:0 context:NULL];
  [self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://akisute.com"]]];
}
- (void)viewWillDisappear:(BOOL)animated
{
  [self.webView.scrollView removeObserver:self forKeyPath:@"contentSize"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
  NSLog(@"%s", __func__);
  NSLog(@"  * contentSize = %@", NSStringFromCGRect(self.webView.scrollView.contentSize));
}
このテクを使うことで、例えばUIWebView自体をスクロールさせないようにした上で通知を受け取ってUIWebViewのframeをどんどん更新するようにし、UIScrollViewの中に埋め込んでしまってUIImageViewやUILabelのようにして使うみたいなUIを作ることが簡単にできます。HTMLの高さに依存するコードも作れますし、なかなか面白いですよ。iOS 5はもちろん、iOS 4でも動作します。(余談で解説)

■余談
UIWebViewのscrollViewプロパティは実はiOS 4のころからPrivate APIとして存在します。ですがiOS 5よりパブリック扱いになったおかげか、iOS 4向けのアプリでscrollViewプロパティを触っていてもリジェクトされたりクラッシュすることなく普通に使えるので非常に嬉しいです。iOS 3では使えませんのであしからず。

2011年9月12日月曜日

UIWebView の Private API を使って BASIC認証のあるページにアクセスする

元ネタはこちら: http://d.hatena.ne.jp/KishikawaKatsumi/20090603/1243968707

仕事でどうしても以下の要件を満たすUIWebViewが必要になったので作りました。
  • 開発環境にBASIC認証がかかっており、そこにUIWebViewでアクセスしたい。
  • アクセス先のHTMLにリンクが埋め込まれているため、URLをhttp://username:password@example.comのように変換することができない。webView:shouldStartLoadWithRequest:navigationType:で頑張ればいける気がしなくともないのですが結局断念しました。
  • 諸事情によりNSURLConnectionが使えない(当然ASIもダメ。あくまでUIWebViewでアクセスする必要がある)。
普通につくるとどうにもうまくいかなかったので、結局UIWebViewをオーバーライドしてPrivate APIを叩く作戦を取ることにしました。

というわけで出来上がったソースコードはこちら。MITライセンスです。
https://gist.github.com/1210372
iOS 4.3.5で動作確認しています。iOS 5でも多分動作します。 iOS 3以下はわかりません><

注意: このコードにはPrivate APIが多分に含まれています。このコードが含まれたアプリをApp Storeに提出しても十中八九審査に通りません。あくまで開発環境での検証用または自分のiPhoneに入れてニヤニヤするコードにのみお使いください。


■しくみ

UIKitをclass-dumpしてたら使えそうなシグネチャを見つけたので、継承してオーバーライドしてゴニョゴニョしてたらできちゃった!って感じです。それにしてもWebKitはPrivate APIにしておくにはもったいない出来の良さですね。このAPIへのフルアクセスがあればAndroidのWebViewと同等かそれ以上に自由に使えるのですが。

2011年4月23日土曜日

iOS 3の UIScrollView はスクロールが発生しない状態では内部の UIView のタッチが取れない

iOS 3以前と iOS 4以降で大きく内部実装が変わったクラスがいくつかあります。 UIScrollView はその中の一つですが、ここではその中でもかなり厄介なバグを紹介します。

iOS 3の UIScrollView は、スクロールが発生しない状態(すなわち UIScrollView.bounds.size >= UIScrollView.contentSize のとき)では内部の UIView のタッチが取れません。タッチが取れないというのは、要するに内部の UIView に実装してある touchesBegan, touchesMoved, touchesEnded などが呼び出されないということです。情報元はこちら。
http://www.iphonedevsdk.com/forum/iphone-sdk-development/1345-uiscrollview-subview-touch-problem.html
Looks like as if the scrollview blocks any touches when the content rectangle is smaller or of same size as its bounds. Really strange behaviour.
ほんと Really strange behavior ですよ Apple センセ。

回避策として、iOS 3では UIScrollView.contentSize を調整して、常にスクロールが発生するような状態にしておけば大丈夫です。 iOS 4以降でしたらこの問題は発生しません。


2011/05/02追記: @k_katsumi さんから突っ込みいただきました。
http://twitter.com/#!/k_katsumi/status/61690186664378368

以下のように delaysContentTouches プロパティと canCancelContentTouches プロパティを NO に設定する方がどうもお行儀がよいようです。
scrollView.delaysContentTouches = NO;
scrollView.canCancelContentTouches = NO;

UIButton でアニメーションする画像を表示させたい

UIButton でアニメーションする画像を表示させたいときは、 UIButton.imageView.animationImages プロパティを使うことで簡単にアニメーションを実装させることができます。さっそくサンプルを書いてみます。

以下、 iOS 3.2 および iOS 4.0 以降にて確認しております。
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];

NSArray *animationImageNames = [NSArray arrayWithObjects:@"animation1.png", @"animation2.png", @"animation3.png", nil];
NSMutableArray *animationImages = [NSMutableArray arrayWithCapacity:[animationImageNames count]];

for (NSString *animationImageName in animationImageNames) {
UIImage *image = [UIImage imageNamed:animationImageName];
[animationImages addObject:image];
}

[button setImage:[animationImages objectAtIndex:0] forState:UIControlStateNormal]; // 大事
button.imageView.animationImages = animationImages;
button.imageView.animationDuration = 2.0;
[button.imageView startAnimating]; // 大事、忘れるとアニメーションしません

[self.view addSubview:button];
UIButton.imageView.animationImages プロパティを使う際に注意する点がいくつかあります。
  • 必ず最初に setImage:forState: メソッドを使って何らかの画像を表示させるようにすること。通常の画像を表示させるようにしないと、 UIButton.imageView が画面に表示されません。
  • iOS 4.0以降ではこのプロパティでセットした画像は UIButton の frame に合わせてリサイズされて表示されますが、 iOS 3.2 以前ではリサイズされずそのままの大きさで表示されますので、注意が必要です。

2011年4月17日日曜日

UINavigationController に管理されている UIViewController の view のサイズを変更したい

たとえばアプリ内の固定の位置に広告を突っ込みたいときなど、 UINavigationController に管理されている UIViewController の view 構造を操作したい場合があると思いますので、調べてみました。


■UINavigationController.viewの中身

iOS 4 ですと以下のようになっています。



最上位の UINavigationController.view に相当するのが UILayoutContainerView で、その下に UINavigationTransitionView があり、そのまた下に UIViewControllerWrapperView というのがあって、こいつが表示される UIViewController.view をラップして表示しているという仕組みになっているようです。

おそらく iOS 3.2 でも同じだと思います。 iOS 3.1.3 以前についてはわかりません。


■実際にやってみる

いろいろ試してみた結果、もっともうまくいきそうなのは以下のように、 viewDidAppear のタイミングで UIViewController.view.superview に対して操作を行う方法であることがわかりました。
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
self.headerView.frame = CGRectMake(0, 0, 320, 100);
self.view.frame = CGRectMake(0, 100, 320, self.view.superview.frame.size.height - 100);
[self.view.superview addSubview:self.headerView];
}

■問題発生

ところがこの方法では画面遷移時のアニメーションが発生すると上記の例で追加した headerView の表示が無茶苦茶に壊れてしまいます。またタイミングを誤ると headerView のサイズが self.view.superview.bounds の大きさに補正されてしまいます。恐らくは UINavigationTransitionView とかいう private な view がなにやらやっているのでしょう。アニメーションが発生しない範囲であれば問題なさそうですが、いまいちです。


■結論

横着しないで表示される UIViewController の view で何とかするしかないみたいですね><

2011年2月28日月曜日

UIWebView のスクロールを制御するためのプロパティを書いてみた

UIWebView にどうして scrollEnabled プロパティがついてねえんだ Apple のチンパンジー野郎!とお嘆きの全国1000万の iOS 開発者の皆様、こんばんわ。もちろん私もその一人であります。

嘆いていてもしょうがないので何とかスクロールを制御する方法を・・・と思って探していたら、すでに2009年の地点で @nakamura001 さんがこんなブログを書いてらっしゃいました。

http://d.hatena.ne.jp/nakamura001/20090520/1242837408

が、遷移先で詳解されている

http://praveenmatanam.wordpress.com/2009/04/03/how-to-disable-uiwebview-from-user-scrolling/

のコードが正直いまいちなのです。何がいまいちって、せっかくのCocoa環境であるにも関わらず、わざわざobjc/runtime.hなんていう低レベルなC言語の関数を使っています。別にパフォーマンスがタイトな場所でもないですし、かっこよくCocoaっぽく書き直してみました。ということで書き直したコードがこちら↓

https://gist.github.com/846258

で、このAdditionを導入するとですね、
UIWebView *webView = [[[UIWebView alloc] initWithFrame:frame] autorelease];
webView.webViewScrollEnabled = NO;
みたいな書き方ができてハッピーになれます。

内部的には NSInvocation を使っています。 Mac OS X 10.5 (iOS 2.0) から存在するこのクラス、本当に便利で、ぶっちゃけ呼び出し対象のシグネチャさえわかれば何でも呼び出せるスグレモノです。フレームワークを作るときなど、呼び出し先のシグネチャしかわからない状況下で対象のメソッドを呼び出す時などに便利な感じかもです。その上 NSInvocationOperation を使ってそのまま並列化もできたりして。
NSMethodSignature *sig = [subview methodSignatureForSelector:selector];
NSInvocation* invocation = [NSInvocation invocationWithMethodSignature:sig];
[invocation setTarget:subview];
[invocation setSelector:selector];
[invocation invoke];
BOOL result = YES;
[invocation getReturnValue:&result];
return result;

2010年4月29日木曜日

iPadアプリに挑戦中

運良くiPadを輸入して手に入れることができましたので、現在iPadアプリの作成にとりかかっています。最初はiPhoneと対して変わりあるまいと思って作っていたのですが、実機で動かしてみると様々な違いや問題が分かってきました。


■今作っている物とか課題とか

現在作っているのはiPad用の紙のノートです。



数年前からシステム手帳を持ち歩いていたのですが、実際スケジューリング等はすべてiPhoneで行っていました。それでも紙を手放せなかった唯一の理由がアイディア出しです。アナログ人間な物で、手で紙に書き付けないとアイディアが出てこないのです。iPhoneのスクリーンは明らかに小さすぎて手書きには不向きでした。

そこでiPadの大きくなった液晶を使えば紙の代わりができるのではないかと思って早速試して見ました。

ペンの色を4色、太さを4種類用意しています。二本指で左右にスワイプするとページをめくることができます。デバイスを横向きにすると、二ページ見開きの状態にすることができます。この見開きのままでも線を書いたりページをめくったりできます。



できるかぎり実際の紙に近づけたかったため、UIを一切排除しています。


■Gesture Recognizer

ページング処理やペン選択ツールの表示にはスワイプジェスチャを使用していますが、今回このジェスチャを実装するためiPhone OS 3.2より新しく搭載された UIGestureRecognizer というクラスを使用してみました。このクラスを使えば各種ジェスチャを自動的に認識してくれるので、自分でいちいちtapの位置を拾って前回のtapとの差分を検出し・・・ということをしなくても済むようになります。

// Add gesture recognizer to the paper view
UISwipeGestureRecognizer *toolPopoverGestureRecognizer = [[[UISwipeGestureRecognizer alloc] initWithTarget:self
action:@selector(handleToolPopover:)] autorelease];
toolPopoverGestureRecognizer.numberOfTouchesRequired = 2;
toolPopoverGestureRecognizer.direction = UISwipeGestureRecognizerDirectionDown | UISwipeGestureRecognizerDirectionUp;
[self.bookView addGestureRecognizer:toolPopoverGestureRecognizer];
しかしながらいくつか問題が。まず認識精度がそれほどよくないです。特に複数指のスワイプに関しては自分で実装した方が精度が出ると思います。また、ジェスチャは通常のタップとは別に検出されているようなので、二本指でタップしてジェスチャしたときには通常の線描画を切るような処理を含めないと、ジェスチャするたびに線が画面に増えてイライラします。


■他のお絵かきアプリのUI

予想通りというか、ふたを開けてみれば他にもたくさんのドロー系アプリがApp Storeにリリースされていたので、それらのUIを見て研究してみる事にしました。

Adobe ideas for iPad



二本指ドラッグでスクロール、ピンチ操作で拡大縮小。一本指で描画中に二本目が触れると即座に描画をキャンセルしてくれるため、誤ってキャンバスに線が増えてしまうということはありません。よく考えられています。

常に左横にツールバーが表示されているようになっています。ツールバーを置くのは邪魔だろうと思っていたのですが、キャンバスの拡大縮小や移動が自由にできればほとんど邪魔にはならないことがわかりました。むしろすぐアクセスできて便利です。

Autodesk SketchBook Pro



こちらも二本指ドラッグでスクロール、ピンチ操作で拡大縮小。一本指で描画中に二本目が触れると即座に描画をキャンセルしてくれるところも全く同じです。

ツールバーを表示するには、画面中央下の小さなポッチをタップするか、または指三本で画面にタップ。この方が画面を広く使えて嬉しい・・・と思っていたのですが、実際に試してみると意外とイライラします。三本指というのが直感的ではないのかもしれません。このへんは人によるのかもしれませんが、私は常にツールバーが表示されている方がスムーズに操作できました。

Penultimate



iWorkと非常に良く似た作りになっていて、しかも操作系はシンプルです。ジェスチャは一切なく、ボタンはツールバーとして常に表示。それはまったく問題ないのですが、画面がスクロールできず拡大縮小もないため、非常に画面が狭く感じ書きづらいです。いくらiPadが大画面とはいえ、ペン先が通常のペンに比べて太い(私が好んで使うペンは0.4mmですが、iPad上の指は10mmぐらい幅があるので、およそ25倍も大きい)ため、せいぜい数文字しか綺麗に書けません。

その他特筆として、ペンの書き味が素晴らしいです。描画速度が速く非常に追従性がよい。すらすら書けます。


■お絵かきアプリのUIまとめ

いくつか使ってみて、さらには自分でも作ってみて感じたのが以下のようなこと。
  • 紙ではなくてホワイトボードのメタファとして使用するとうまくいく
  • 拡大・縮小・スクロールは絶対必須 無いとアプリとして成立しない
  • ジェスチャを使ってメニューを出すのは思ったよりも効果的ではない
  • 書き味はや動作速度は大事、使っていて楽しくなる
まず一番目。ペン先が太く、消しゴム削除がたやすく、アンドゥリドゥもできるので、書いている間隔がホワイトボードに近い気がします。そのため紙ではなくてホワイトボードだと思って実装するとよさそうです。

二番目。ペン先がやたら太いわりには画面サイズが1024x768しかないので、拡大縮小スクロールできないと話になりません。逆にこれができれば事実上キャンバスサイズは無限大にできるわけで、デバイスサイズ以上の活躍をしてくれます。

三番目。ジェスチャ自体は上手く使えば非常に有効です。たとえばアンドゥリドゥなどの操作はジェスチャで行う方が直感的でした。しかしメニューは常時画面に表示していた方が良い気がします。

最後、四番目。動作速度は極めて大事だと感じました。特に描画速度は最も大事で、線を引くのがもっさりしてしまうとそのせいで綺麗な曲線にならなかったり、単純にイライラしたりします。実物のホワイトボードに書くぐらいの速さで描画ができるように目指したいです。

2009年7月19日日曜日

iPhone付属のPhotos(写真)アプリのような、回転可能な全画面表示ビューを作る方法



Photos(写真)アプリに使われている、全画面ビューを真似して作ってみました。具体的には以下のような仕様になります。
  • ステータスバーの後ろも含め、ビューの内容が全画面(320x480)で表示される
  • 画面をタップするとステータスバーとナビゲーションバーが消える
  • もう一度タップすると再度表示される
  • iPhoneを傾けると画面が回転する
参考にしたページは以下のとおり。
http://life.ponpoko.tv/?p=533

また、サンプルソースも公開してます。こちらのソースの、- (void)fullScreenMode:(BOOL)mode animated:(BOOL)animatedがフルスクリーン表示を実現しているメソッドになります。


■注意
iPhone OS 3.0以上専用です。2.2以下では別の方法を取る必要があります。


■まずはStatus BarとNavigation Barを消す
http://life.ponpoko.tv/?p=533 こちらのページに書いてある内容を100%そのままパク参考にさせていただければ何も問題ありません。iPhone OS 2.2までは記載されている通りのworkaroundが必要になりますが、3.0以降ではバグが修正されているため、単にStatus BarとNavigation Barを消せばOKです。

ただし一点、[self.navigationController setNavigationBarHidden:YES animated:YES];を使うと、ナビゲーションバーが上方向に対してぴょこんと引っ込むようなアニメーションになります。ステータスバーはその場でフェードアウトして消えるので、なんだか不自然な感じに見えます。Photos.app(写真アプリ)など、iPhone標準のアプリと同様にナビゲーションバーをフェードアウトさせて消したい場合には、UIViewのアニメーション機能とalpha値を利用して以下のようにします。
    if (animated)
{
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:0.3];
}
self.navigationController.navigationBar.alpha = mode ? 0 : 1;
if (animated)
{
[UIView commitAnimations];
}
Three20プロジェクトのソースから拝借してきました><


■Viewを全画面配置する(Status BarとNavigation Barのあった場所にも描画させたい)
さんざん悩んだあげく、実はUIViewControllerにそのためのプロパティがあったと知り、ものすごく簡単に解決しました。
- (void)viewWillAppear:(BOOL)animated
{
[UIApplication sharedApplication].statusBarStyle = UIStatusBarStyleBlackTranslucent;
self.navigationController.navigationBar.barStyle = UIBarStyleBlackTranslucent;
// このUIViewController.wantsFullScreenLayoutをYESにするとフルスクリーン表示になる
self.wantsFullScreenLayout = YES;
}
ついでにステータスバーとナビゲーションバーを透明にして裏が透過するようにしてみました。


■ビューの回転に対応したい
何も考えずにshouldAutorotateToInterfaceOrientationすればいいだろう常考
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
return YES;
}
とおもったら思わぬ落とし穴が。確かに全画面表示にした状態でビューを回転させたり、逆にナビゲーションバーが出ている状態でビューを回転させたりする分にはこれだけで全く問題ないのですが、
  1. ナビゲーションバーを消す(全画面表示にする)
  2. iPhoneを傾けてビューを回転させる
  3. 再度ナビゲーションバーを表示する
という手順を踏むと、再度表示したときにナビゲーションバーが崩れる問題が発生。結局ナビゲーションバーを消したり再表示したりする際に、以下のようなworkaroundをするハメに。
    // Force set the frame of the navigation bar
CGRect frame = self.navigationController.navigationBar.frame;
frame.origin.y = 20.0;
self.navigationController.navigationBar.frame = frame;


■全画面表示をしている画面から前の画面に戻るとバグる問題
これで完成かと思えば、最後にもう一つ落とし穴が。
  1. iPhoneを横向きに傾けて、Landscape表示にする
  2. ナビゲーションバーを表示する
  3. そのまま横向きになっている状態で、ナビゲーションバーの戻るボタンを押して、前の画面に戻る
  4. 前の画面に戻る際に、wantsFullScreenLayoutをNOにして通常画面モードにし、ステータスバーとナビゲーションバーのスタイルもDefaultに戻す
という風な操作を行うと、なぜかナビゲーションバーのスタイルだけがDefaultに戻らず、表示も崩れるという問題が発生しました。たぶんOS 3.0のバグだと思います。
いろいろ試してみた結果、全画面表示をしている画面ではなく、前の画面のviewWillAppearのタイミングで以下のコードを呼びだせば問題が解決しました。
- (void)viewWillAppear:(BOOL)animated
{
[UIApplication sharedApplication].statusBarStyle = UIStatusBarStyleDefault;
self.navigationController.navigationBar.barStyle = UIBarStyleDefault;
}

CGGradientを用いてUITableViewCellを描画し、テーブルをカッコよく見せる方法

デフォルトのUITableViewCellの背景が白くてのっぺりでいまいち味気ないと思い、背景にグラデーションを付けてかっこよく見せる方法を調べてみました。単純に別途用意した背景画像をbackgroundViewに表示してもよいのですが、Cocoa Touchの2Dグラフィックスライブラリにはグラデーションを描画するためのCGGradientというクラスが最初から用意されています。さっそく私もパクってインスパイアされてやってみました。

参考にしたページはこちら。
http://developer.apple.com/documentation/graphicsimaging/conceptual/drawingwithquartz2d/dq_shadings/dq_shadings.html#//apple_ref/doc/uid/TP30001066-CH207-TPXREF101


■どこで描画するか
  • UITableViewCellのdrawRectで直接描画。
    少しでも高速に描画したい場合にはこの方法
  • 新規にUIViewを継承した背景用Viewを作成しセルのbackgroundViewに設定。そのViewのdrawRectで描画
    複数のUITableViewCellで同じ背景を適用したいときはこの方法が便利

■まずは実際に描画してみる
drawRectの中でCGContextを作成して、続いてCGGradientを生成。CGGradientを作るためにはCGColorSpaceとか色を指定する配列とかが必要になるのでそれらも生成。最後に生成したCGGradientオブジェクトを描画するという流れになります。ということでソースを見てみましょう。
- (void)drawRect:(CGRect)rect
{
// CGContextを用意する
CGContextRef context = UIGraphicsGetCurrentContext();

// CGGradientを生成する
// 生成するためにCGColorSpaceと色データの配列が必要になるので
// 適当に用意する
CGGradientRef gradient;
CGColorSpaceRef colorSpace;
size_t num_locations = 2;
CGFloat locations[2] = { 0.0, 1.0 };
CGFloat components[8] = { 1.0, 1.0, 1.0, 1.0, // Start color
0.79, 0.79, 0.79, 1.0 }; // End color
colorSpace = CGColorSpaceCreateDeviceRGB();
gradient = CGGradientCreateWithColorComponents(colorSpace, components,
locations, num_locations);

// 生成したCGGradientを描画する
// 始点と終点を指定してやると、その間に直線的なグラデーションが描画される。
// (横幅は無限大)
CGPoint startPoint = CGPointMake(self.frame.size.width/2, 0.0);
CGPoint endPoint = CGPointMake(self.frame.size.width/2, self.frame.size.height);
CGContextDrawLinearGradient(context, gradient, startPoint, endPoint, 0);

// GradientとColorSpaceを開放する
CGColorSpaceRelease(colorSpace);
CGGradientRelease(gradient);
}
このコードを実行すると・・・


はい!もうグラデーションができました。嘘みたいに簡単です。

2010/05/24追記:注意点として、CGGradientとCGColorRefのオブジェクトは手動でリリースしないとメモリリークが発生します!


■UIColorを元にグラデーションを作る
先ほどの例では配列にRGBA要素を渡してグラデーションを作りましたが、UIColorが使えるともっとお手軽で、しかもRGBAだけではなくてHSBAで色が指定できて何かと便利です。ということで、次はUIColorからグラデーションを作ってみます。

UIColorから直接CGGradientを作ることは出来ないので、途中でCGColorとCFArrayRefを作り、それを元にCGGradientを生成してみます。コードはこちら。
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
UIColor *currentColor = [UIColor colorWithHue:currentBackgroundColorHSBA[0]
saturation:currentBackgroundColorHSBA[1]
brightness:currentBackgroundColorHSBA[2]
alpha:currentBackgroundColorHSBA[3]];
// gradient background
CGGradientRef grad;
CGColorSpaceRef colorSpace;

// UIColorからCGColorを取り出すのはとっても簡単
CGColorRef currentColorRef = [currentColor CGColor];
CGColorRef voidColorRef = [[UIColor colorWithHue:0.0
saturation:0.0
brightness:0.17
alpha:1.0] CGColor];
CGColorRef colorArray[2] = {voidColorRef, currentColorRef};
CGFloat locations[2] = { 0.0, 1.0 };

// CFArrayRefを作る。
// もっと簡単に作りたければ、NSArrayを作ってからCFArrayRefにキャストするだけでもよい。(未確認)
CFArrayRef colors = CFArrayCreate(kCFAllocatorDefault, (const void **)colorArray, 2, &kCFTypeArrayCallBacks);

colorSpace = CGColorSpaceCreateDeviceRGB();
// Gradientを生成する
grad = CGGradientCreateWithColors(colorSpace, colors, locations);

// 描画する
CGFloat progress = currentTime / allottedTime;
CGFloat height = self.frame.size.height + HEIGHT_GRADIENT;
CGPoint startPoint = CGPointMake(self.frame.size.width/2, progress * height - HEIGHT_GRADIENT);
CGPoint endPoint = CGPointMake(self.frame.size.width/2, progress * height);
CGContextDrawLinearGradient(context,
grad,
startPoint,
endPoint,
kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation);
// GradientとColorSpaceを開放する
CGColorSpaceRelease(colorSpace);
CGGradientRelease(grad);
}
CGContextDrawLinearGradientの第4引数にkCGGradientDrawsBeforeStartLocationフラグとkCGGradientDrawsAfterEndLocationフラグを設定しています。これらのフラグを指定すると、startPoint以前、およびendPoint以降をグラデーションの開始色と終了色でべた塗りしてくれます。実行結果は以下の通り。


こんな感じで、画面中央から下が全部べた塗りになっているのが分かると思います。

他にも、CGGradientを作る際にlocationsを増やせば2色だけではなくて多色のグラデーションを作成することが出来たりします。このあたり、Appleが公開しているドキュメントだけでも相当詳しく紹介されているので、そちらを見ればほぼ間違いないかと。


■サンプルソース
今回のサンプルはgithubですべてソースを公開して居ますので、より詳しく学びたい方はそちらも併せてご参照ください。
http://github.com/akisute/YourTurn/tree/master
これとかこれを見るとよいかと思います。

2009年7月12日日曜日

EditControlとAccessoryViewの背景は透明



  • EditControlの背景はデフォルトで透明
  • AccessoryViewの背景もデフォルトで透明
  • UILabelの背景はデフォルトで白塗りつぶし(透明にはならない)

OS 2.2.1での実験結果なので、3.0では違うかも(たぶん同じ)

それだけです。お粗末様でした・・・

2009年6月8日月曜日

時間を入力するために、カスタムUIPickerViewを作ってみた



時間を入力するためのUIが欲しかったので、こんな感じのカスタムUIPickerViewを作ってみました。ソースコードはこちら。
http://github.com/akisute/YourTurn/blob/8119bf028acf4908edb602d277544bc2cf6a5848/Classes/YTTimePickerView.h
http://github.com/akisute/YourTurn/blob/8119bf028acf4908edb602d277544bc2cf6a5848/Classes/YTTimePickerView.m


使い方はソースを見ていただければきっとご理解いただけると信じておりますので、UIPickerViewを使っていて気づいた点などをいくつか晒してみたいと思います。


■参考にした記事
id:paellaさんのダイアリーを参考にさせていただきました。ありがとうございます。
http://iphone-dev.g.hatena.ne.jp/paella/20090521#20090521fn1

基本的にはこちらの記事にまとめられている内容に従って、
// 2) ビューを返したい場合
- (UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row forComponent:(NSInteger)component reusingView:(UIView *)view {}
このデリゲートメソッドを使いカスタムViewを利用した実装を行ったのですが、一部私の実装とpaellaさんの記事で異なっている点があるので、ご紹介したいと思います。


■UILabelを直接デリゲートメソッドから返す
元記事では、
UIViewを返すデリゲート、必ずUIViewを返さないとうまく動いてくれません。このサイトやこのサイトで紹介されているようなUILabelを直接渡してあげる方法は、少なくとも私の環境(2.2.1)では何も表示されませんでした。
と、UIViewを返さなくてはダメとご指摘がありますが、どうやらUILabelでも大丈夫みたいです。ビルドターゲット = iPhone OS 2.2.1の環境で確認できました。

UILabelをそのままデリゲートから返却したときに画面に表示されなかった原因は、どうやらUILabelのframe.size.heightが0になってしまっていた(要するにつぶれてしまっていた)のが原因みたいです。そこで、以下のようなコーディングを行いました。
- (UIView *)pickerView:(UIPickerView *)picker viewForRow:(NSInteger)row forComponent:(NSInteger)component reusingView:(UIView *)view
{
UILabel *label = (UILabel *)view;
if (!label)
{
label = [[[UILabel alloc] init] autorelease];
label.frame = CGRectMake(label.frame.origin.x, label.frame.origin.y, 20.0, 23.0);
label.backgroundColor = [UIColor clearColor];
// 以下初期化コードなど・・・
}

return label;
}
このように、label.frameの高さを無理矢理指定してやれば、きちんと画面に出せるみたいです。また、この方法を使えば、元記事で
ポイントはビューの座標。どうも各要素での{0,0}の位置はPicker内各Rowのど真ん中にあるらしく、サンプルのように負数を指定してあげないとどこかに寄った状態になってしまうみたいです。
とご指摘があったビューの座標も、特に気にせず普通に指定することが出来るようになりました。


■Pickerを、UIScrollViewのサブクラス(UITableViewなど)にaddSubviewする場合の注意点
この記事の見出しに貼り付けた画像では、作成したカスタムPickerをGroupedスタイルなUITableViewのfooterViewに貼り付けています。
    // Initialize time picker with a previously selected value
timePicker = [[YTTimePickerView alloc] initWithFrame:CGRectZero];
NSInteger initialValue = [[NSUserDefaults standardUserDefaults] integerForKey:_USERDEFAULTS_TIMEPICKER_INITIALVALUE];
timePicker.time = (initialValue == 0) ? 300 : initialValue;
timePicker.timePickerViewDelegate = self;
[timePicker selectRowWithCurrentTime];
self.tableView.tableFooterView = timePicker;
このように、UIScrollViewのサブクラスのサブビューとしてUIPickerViewを貼り付けると、そのままの状態ではうまくPickerのドラムが回転してくれないという症状が発生してしまいます。
これはどうやらUIPickerViewのスクロールとUIScrollViewのスクロールがけんかしてしまっているみたいです。そこで、スクロールする必要のないTableView側の設定をOFFにします。具体的には、Interface Builderを利用する場合、以下の赤枠でくくった箇所を設定すると良いようです。



こうすることで、綺麗にドラムが回転してくれるようになります。

2009年5月19日火曜日

UINavigationの片方のサイドに複数個のボタンを持たせたい



こんな感じでUINavigationBarの片方のサイドに複数個のボタンをおく方法を探してみました。UIToolBarと異なり、UINavigationBarではUINavigationItem.rightBarButtonItem, UINavigationItem.leftBarButtonItem, それからタイトル部分と、最大でも3個しかアイテムを配置することが出来ません。そのため、複数のボタンを一つの配置箇所にまとめて配置したい場合には、カスタムビューを作成する必要があります。
Appleの配布しているデモアプリケーションにもありますが、こういう場合にはUISegmentedControlのmomentaryプロパティをYESに指定して、ボタンみたいに利用するのがいちばん良いようです。
NSArray *items = [NSArray arrayWithObjects:@"Add", @"YourTurn", nil];
UISegmentedControl *segmentedControl = [[[UISegmentedControl alloc] initWithItems:items] autorelease];
segmentedControl.selectedSegmentIndex = UISegmentedControlNoSegment;
segmentedControl.segmentedControlStyle = UISegmentedControlStyleBar;
segmentedControl.momentary = YES;
[segmentedControl addTarget:self action:@selector(segmentedControlClicked:) forControlEvents:UIControlEventValueChanged];
self.navigationItem.rightBarButtonItem = [[[UIBarButtonItem alloc] initWithCustomView:segmentedControl] autorelease];

この方法でひとまずはUINavigationItemの右側に複数のボタンを作ることが出来ました。しかしまだまだ完璧ではなくて、現地点で分かっている限りでも二つの問題があります。
  1. ボタンの背景色が通常のボタンとは異なる色になる
  2. 個々のボタンひとつひとつを個別にDisableすることができない

Appigo Todoなんかは普通にUINavigationBarの右側に3つもまとめてボタンを綺麗に配置してあったりするのですが、一体全体どうやってるんでしょうね?・・・うーん。

2008年11月30日日曜日

CS193P Cocoa Programming - 7日目 UINavigationViewControllerを使う

  • UIViewControllerのawakeFromNibは動作しないときがある。UIViewControllerの初期化には 必ずviewDidLoadを使うこと
  • UINavigationViewControllerはxibファイルに含めずに、ソースコード中で初期化したり管理する方がうまくいく
  • UINavigationViewControllerの上に表示されるバーを編集したいとき(ボタンを追加したりするとき)は、UINavigationViewControllerにpushされるUIViewControllerのnavigationItemプロパティを編集する。Interface Builderから操作できるかどうかはわからない
  • UIAlertViewがポップアップ表示、UIActionSheetが下からにょきっと出てくるボタンのリストを表示
  • ボタンにソースコード中からアクションを追加することが可能(UIButton, UIBarButtonなどのドキュメントを参照のこと)


さて、ナビゲーションがついてようやくiPhoneアプリらしくなってきました。
画面はナビゲーション時にUIViewControllerのインスタンスがどのように管理されているかをログに吐いてみたところです。viewDidDisappearの後にdeallocが毎回流されているのがわかります。
要するに、一つのviewを毎回毎回alloc->initしないでインスタンスを再利用してやろうと思うときはUINavigtionViewControllerにpushするだけではなくて、どこか別のところでUIViewControllerのインスタンスを保持しておく必要があるようです。2tchの作者さん曰くalloc->initは相当重い動作らしいので、できるかぎり使い回しができるようにしたいです。



ポップアップも出るようになりました。


※いい加減開発ペースと学習ペースをあげようと思っているので、しばらくの間はブログの更新がこんな感じで適当になりそうです。

2008年11月26日水曜日

CS193P Cocoa Programming - 6日目おまけ、多角形をくるくる回せるようにしてみた

  • CS193P(http://www.stanford.edu/class/cs193p/cgi-bin/index.php)のチュートリアルで作っているHelloPolyプロジェクトを自分なりにアレンジしてみた
  • 自分なりにアレンジしてみた=ニコニコ動画だと駄作フラグ
  • UIViewはhiddenプロパティをYESにした瞬間に消えてしまうので、アニメーションでフェードアウトさせたいときは、まずアニメーションだけ実行>アニメーション終了時のデリゲータ(- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag)で実際にUIViewをhiddenにする
  • タッチ動作を取得するには、UIView(正確にはUIResponder)のtouchBeganメソッドやtouchMovedメソッドをオーバーライドする
  • デフォルトではマルチタッチ不可能(最初の1タッチのみ感知する)、マルチタッチしたければ設定を変えること
  • UITouch.tapCountでタップした数を取得できる。これを使ってダブルタップを検出できるが、連打すると3とか4とか2より大きい数が取得されてしまうので注意
  • CGPointはクラスではなくて構造体、頭にCGがつくCore Graphics系はすべて純粋C言語であるところに注意!
  • CGPointなどを作成するときはCGPointMake()関数を使う
  • CGPointやCGRectなどをObjective-Cのクラスとして扱いたいときはNSValueクラスを使う、たとえば[NSValue valueWithCGPoint:(CGPoint)point]など
  • CGRectGetMidX()関数とか地味に超便利
  • C言語の変数の有効範囲について:http://www.cis1.c.dendai.ac.jp/c_master/C_14.htm
    CGPointMake()で作ったCGPointなどは自動変数なので、別のメソッドに渡すときはポインタ渡しではなくてそのまま値で渡す

ただ単にカリキュラムにそって進めていくだけでは面白くないので、この辺りでちょっとチャレンジングなことをしてみることにしました


まずは新しくサブビューを追加。


ON OFFスイッチでビューを出したり消したり。出したり消したりするときはアニメーションします。


UISliderを使って、線の太さを変えてみましたよ。
UISliderのvalueプロパティはdouble型なので注意です。ずっとNSString型だと思ってました。
それから、タップした向きに多角形を回転できるようにしました。赤線は中心からタップした点への線分です。
タップしてドラッグするとスムー(?)ズに回転しますよ。


破線への切り替えもできるようにしました。
UISegmentedControlを使っています。このUISegmentedControl、取得できる値が選択されているセグメントのインデックス番号(selectedSegmentIndex)だけなのでちょっと厄介です。HTMLのラジオボタンみたいに好きな値をセグメントごとに持たせられればいいのに。


今回作成したアプリのプロジェクトファイルを公開してみました。
面白いことをやっている点は何一つないのですが、まぁ一応。
http://sites.google.com/site/akisutesama/files/HelloPoly-06b.zip?attredirects=0

今後はgithubとかで公開できるようにします。

2008年11月22日土曜日

CS193P Cocoa Programming - 5日目、いよいよView自作

  • Interface Builderを使ったらとにかくWrite Class Files...を押すのを忘れないように
  • 描画はJavaのSwingなどとほぼ同じ
  • drawRectメソッドをオーバーライドして描画する
ViewとかAnimationとか、いよいよ実践が近くなってきたようです。
だんだんと難易度も上がってきました。



まずは自作Viewを作成。
Interface BuilderからUIViewを引っ張ってきて画面上に配置。
クラス名を適当に(ここではPolygonView)に変更。
ControllerクラスやらModelクラスなど、必要に応じてOutletを追加します。
最後に(一番忘れやすいんだけれども)メニューからFile > Write Class Files... この書き出しを行わないとXcode上に実際のソースが出てきません。

書き出しを行ったらXcodeを開いて、以下の2つの仕事を行います。
・先ほど追加したPolygonViewのスーパークラスを定義する
・ControllerにIBOutlet PolygonView *polygonViewを追加する

新しいオブジェクトはWrite Class Filesで一発だと思うけれども、既存のクラス(例えば前回実装したController)なんかは、書き出ししちゃうと上書きしてしまいそう。怖いので今回はXcodeから書くことにしました。でもこれ非常に非効率的。hファイルだけ上書きしてほしいんですけど・・・



勇気を振り絞って上書きWrite Class Files...にチャレンジしたら、なんだかこんなFileMergeとかいうアプリが立ち上がって、きれいにマージすることができましたとさ。よかったよかった。


では早速Viewに描画処理を追加します。
描画を行うのはUIViewの-(void)drawRect:(CGRect)rectメソッドですので、こいつをオーバーライド。
あとは再描画したいタイミングでUIViewのsetNeedsDisplayをコールすれば適切なタイミングでシステムが再描画してくれるというしくみ。Swingに似てますね。



実際に描画してみました。簡単簡単!!



描画のやり方さえわかってしまえばこっちのものです。
ポリゴンの点の位置を計算するメソッドは例題の中で用意されていたので、それを丸コピーして点の位置に線を引くだけ。線の太さを調節する関数はCGContextSetLineWidthというのが見つかったのでそれを使うだけ。リファレンスが使いやすい!すてき!



カスタムビューの中にUILabelを追加して、ラベルにポリゴンの名称を表示できるようにしてみました。UILabel.textプロパティを書き換えるタイミングはカスタムビューのdrawRectの中で。ほかに良いタイミングが見当たらず。


ひとまずこんなところですかね。次は・・・
  • スライダーで線の太さを変える
  • 線の種類を何らかのスイッチで変える
  • ビューの上でフリックしたらポリゴンがくるくる回るようにする(アニメーション処理の勉強が必要)
これらを試してみたいですね。

2008年11月17日月曜日

CS193P Cocoa Programming - 現在4日目

  • メモリの管理についてお勉強
  • allocで領域確保(オーバーライドしない)
  • initで初期化(オーバーライドして使う)
  • 同一オブジェクトを参照するときはretain(オーバーライドしない)
  • コピーするときはcopy(オーバーライドしない、copyWithZoneをオーバーライドするべき)
  • 解放するときはrelease(オーバーライドしない)
  • 実際にメモリから削除されるのはdealloc(オーバーライドして使う)
  • NSAutoReleasePoolとautoreleaseメソッド・・・事前にautoreleaseしておいたオブジェクトは[NSAutoReleasePool releaseまたはdrain]呼び出し時に一斉に解放される

1日に二日分前進。iPhone発売から既に4ヶ月経過、既に無数の超優良アプリがわんさかと出回っています。果たして私は遅れを取り戻せるでしょうか。


これはNSURLから文字列を取得する方法を模索していたときです。
結局、absoluteStringかrelativeStringが正解ということがわかりました。


クラスの作成とメモリ管理。ようやくCらしくなってきたかな?
Objective-Cではガベージコレクションもできるらしいのですが、iPhone開発ではガベージコレクタをぶん回せるだけのリソースはないため昔ながらのメモリ管理でやる必要があります。



いろんなイニシャライザをためしてみました。
リストにオブジェクトを突っ込んだときは、突っ込んでいるNSArray自身もリリースしないと、個々のオブジェクトがリリースされません。おそらく内部でretainされてるんでしょうね。



introspection(Javaでいうところのrefrection)にチャレンジ。instanceof演算子みたいなのはなくて、すべてNSObjectのメソッドとして提供されています。java.lang.refrectionパッケージみたいな面倒さはないです。あとセレクタはSEL型とかいう専用の型があって便利。いわゆる関数へのポインタとして使えます。Javaの何が不満ってこの関数ポインタがないところですよ。



続けてもう一つの宿題のほう(Assignment2B)に突入。いよいよ実際に動作するiPhoneアプリを作ることになるのですが、その前に一つ宿題にバグ?があったのでご報告。
上の画像のカーソルで選択している部分(PolygonShape.h)、Assignment2Aで作成したときにはCocoa/Cocoa.hをインポートしていますが、2BではCocoaが使えない(iPhoneアプリになるため)ので、Foundation/Foundation.hに書き換える必要がありました。



で、これが完成品です。
SwingやWin32API開発並みの苦行を覚悟していたのですが、あまりに簡単で逆に拍子抜け。はまりそうな点といえば、プロジェクトに新しくクラスをインポートしたり作成した後はInterface Builderからアプリケーションに登録しなければならないというところぐらいでしょうか。あと、awakeFromNibはコントローラに実装しないとだめ(間違えてPolygonShapeに一生懸命実装して、ビルド直前にこの過ちに気づきました)。


今のところは楽勝。問題はOpen GL ESに手を出すあたりからでしょうかね。

2008年11月9日日曜日

CS193P Cocoa Programming - 1日目の宿題Aをやってみた

  • http://www.stanford.edu/class/cs193p/cgi-bin/index.php
  • これはわかりやすい
  • 宿題があるのが実にうれしい
  • Interface Builderで配置したクラスの属性(位置とか表示するイメージとか)を操作するときは、Command + Shift + I またはCommand + 1から4
  • Labelの文字の大きさとフォントは変えられたが色とか太字はなぜかうまくいかず
  • vertical centerな配置やholizonal centerな配置を行うには、メニューのLayout > Align
今日からスタンフォード大学の学生になった気分でさくさくっと勉強。
手始めに1日目の宿題から。・・・こんなに宿題を嬉々としながらこなす私の姿を学生時代の自分に見せてやりたいものだ。
一つ目の宿題は、コーディングはいっさいなく、interface builderからCocoa Touchが用意しているviewやclassを配置して画面をデザインしてビルドして走らせるだけというもの。


こんな感じでinterface builderにパーツを配置して・・・


できた!初日の宿題だけあって実に簡単。

こういったグラフィカルなGUI作成機能は別に目新しいものではない(Microsoft Visual Studio 2005などでもできる)が、これまで使ったことがあるツールよりは使いやすかった気がする。まぁVS2005はマイクロソフト語で書かれているので読めない=論外だし、Eclipse GEFはそもそもSwing自体があまりよいフレームワークでなかったため使いづらかった。
なによりiPhoneの場合はデバイスの画面サイズが決まっているから絶対座標指定がしやすい。