今回、かなり脱線します。
ということで、始まり始まり~。
前回、サムネイルが入りきらない場合スクロールさせるって書いたけど、これには、UIScrollViewを使うわけです。
なんですが、せっかくなんでUIScrollView使う前に、UIViewでUIScrollViewっぽいことやってみます。UIScrollViewってのは、UIViewの入れ子機能やframe、boundsの機能をうまく使って実現してるっぽいんで、ここらへんを自分でやってみて原理を勉強しちゃおうってわけですわ。
で、まずpuzzlerAppDelegateでノーマルのUIViewControllerを作ってたのをやめて、サムネイル表示用のカスタムUIViewControllerを使うところから。
puzzlerAppDelegate.m内に書いてもいいんだけど、どのみち単体のファイルにするつもりなんで、ここで作っておきます。
というわけで、前回のpuzzler.xcodeprojを立ち上げて、いつものようにFile→New→New Fileメニューで出てくる画面でUIViewControllerテンプレートを選択~。
次に出てくる画面でSubclass ofをUIViewControllerにして~
XIBは作らないのでwith XIB for user interfaceのチェックも外す
名前をThumbnailViewController.mにして保存。
保存先はpuzzler/puzzler/フォルダ内あたりが妥当
ばっちりpuzzler.xcodeprojにThumbnailViewController.m/hが加わったら
puzzlerAppDelegate.mを変更だぜ!
といってもThumbnailViewController.hをimportして[UIViewController alloc]を[ThumbnailViewController alloc]に変えるだけ。
これで、ThumbnailViewControllerが使われることになるんで、あとはThumbnailViewController側のviewにサムネイル用のUIViewを入れ子にして貼付けていく。
receivedThumbControllerやprivateThumbControllerの型がUIViewController*にままだけど、特に変える必要はないです。ThumbnailViewControllerはUIViewControllerを継承してるんで、なんの間違いもないわけですな。
あとはThumbnailViewController側で、viewに子供のUIViewを追加していく作業。
ちょっと試しに真っ黒いビューを1個追加します。
余談だけど、この時点でかなりのビューがありますな。
うそ~ん、と思う人は、今回のサンプルのpuzzlerAppDelegate.m、-application:didFinishLaunchingWithOptions:メソッドの最後にある
の注釈を外してみましょう。
この追加する黒いビューは実験用です。サムネイルビューを乗せていろいろ実験するので、ThumbnailViewControllerのインスタンス変数にします。
で、viewにbasegroundViewを追加するタイミングなんですが、-initWithNibName:bundle:メソッドではやらないように。
こんなふうになー
なんでかっていうと、UIViewControllerてのは、メモリが厳しくなってきて、その時に自分のviewが画面上に表示されていなければ、viewを解放しちゃうんですな。
試すのは簡単で、アプリ起動してハードウェア→メモリ警告をシミュレートメニュー選ぶだけ。
何にも起きねーじゃんと思うだろうけど、それは「自分のviewが画に表示されていなければ」という条件にあてはまらないから。あてはまるのは、見えてない方。今回なら「自分のもの」タブ側ってことです。「自分のもの」タブに切り替えて見ると~
ハイ消えたー、ドン
ちゅーわけで、青い背景色のビューは白い背景色のビューに変わっちゃてるし、basegroundViewも消えちゃうわけです。この状態でもう一回、ハードウェア→メモリ警告をシミュレートメニュー選んで、「貰い物」タブに切り替えると、赤い方もきっちり真っ白になります。
こんなふうに、UIViewControllerはメモリ厳しくなると「自分のviewが画面上に表示されていなければ」自分のview解放しちゃって、次に自分のviewが必要になると、あらためて新しく作るって事をします。
ゆく河の流れは絶えずして、しかももとの水にあらず
そもそもUIViewControllerの役割りはそういうもんで、UIViewのマネージャみたいなもんなわけです。- touchesBegan: withEvent:メソッドとかがあるんで、UIViewの一種だと思ってた人は、悔い改めるように。ぜんぜん違うから。
そういう視点でいうと、puzzlerAppDelegate.mの- application:didFinishLaunchingWithOptionsメソッドでやってるreceivedThumbController.viewに背景色を指定してるのも大間違いではあるんですが、いちいちプロパティ用意するのが面倒だったんでね。承知でやってるって事でよろしく。
まあ、そういうわけで、viewにbasegroundViewを追加するタイミングは-initWithNibName:bundle:メソッドでは駄目って事になります。
じゃ、どうするかというと、UIViewControllerは、自分のviewを作った直後に-viewDidLoadメソッドを呼ぶようになっているんで、そこで追加します。
こっちは、viewがあらためて作り直されるたびに呼ばれるので、何回、ハードウェア→メモリ警告をシミュレートメニュー呼ばれても問題ないです。ま、puzzlerAppDelegateでやった背景色は解除されちゃうけどな。というか解除ではなくviewそのものが別物になっとるわけです。
そこらへん気になる人は、ThumbnailViewControllerクラスに@propertyで背景色設定できるようにして、-viewDidLoadメソッドで、その背景色をviewに設定するようにすればいい。@property?な人は「iPhoneアプリ開発、号外 propertyをちょっと調べてみた」を読みましょう。ただし結構適当な内容、すまそ。
前に貼付けたbasegroundViewがどうなるかというと、前のviewが解放される時点で一緒に解放されます。autoreleaseやaddSubvewの後にreleaseってしていない場合は、メモリ上に残ってしまうので注意。特に実際にサムネイル画像使ってる場合、basegroundViewにはUIImageを持ったUIImageViewをたっぷりと貼る事になるんで、それがまるまるメモリ上に残る事になる。
それは、具合が悪いんでaddSubview:でself.viewにオーナーシップを渡したら、自分の方は管理を放棄してるわけです。
オーナーシップ?な人は日本語訳サイトの「Cocoaメモリ管理プログラミングガイド」を読みましょう。「iOSデバッグ&最適化技法 for iPad/iPhone」でも可!
ま、解放された場合、次に画面上に表示する時はビューの再構築が発生、複雑な構成であればあるほど、作りなおすのに時間がかかる、ユーザーはイライラするってわけで、これはメモリを取るか、スピードを取るかって話なわけですな。
話を元に戻そう。
このbasegroundViewに入れ子状でUIViewを貼付けるとどうなるかってのが、今日の本題なわけです。
実行すると、こんな感じ。
わざと、basegroundViewからはみ出した分も表示させてる
で、ここで入れ子になったUIVewiは、親側のUIViewのbounds座標形に従うって特性を利用して、basegroundView.boundsの原点をずらすとどうなるかってのが今回の実験です。
て感じで、原点を500ポイントほどずらすとどうなるかってことですよ。見た目を劇的にしたかったんで、アニメーションでやってみます。
でもって、タブ切り替えるたびに繰り返すように、viewが表示された時に呼ばれる-viewDidAppear:メソッドでやってみます。
-viewDidLoadメソッドは、viewが作成された直後、-viewDidAppear:はviewが画面に現れた直後って違いを忘れないように。
-animateWithDuration:animationsはブロック構文版のbeginAnimation~commitAnimationsです。ブロック構文はその(227)、beginAnimation~commitAnimationsはその(37)あたりを見てね。commitAnimationsでブログ内検索すると、いろいろなところでヒットもする。
実行すると、タブ切り替えるたびにbasegroundViewに貼付けたUIViewが上にスクロールする。
いかにもbasegroundViewの内部が動いてるようにしたい人はbasegroundView.clipsToBoundsをYESにしましょう。
でもって、UIScrollViewみたいにフリックに対応して慣性スクロールしてるっぽくしたいならUISwipeGestureRecognizerを使って、上下スワイプジェスチャー見張って、スワイプ発見したらさっきのアニメーションを向きにあわせてやっちゃうわけです。
ここで使ってるUISwipeGestureRecognizerてのは、ユーザーのスワイプジェスチャーを察知したらinitWithTarget:で指定したオブジェクトのaction:で指定したメソッドを呼んでくれるってクラスです。
実際のUIScrollViewは、もっときめ細かい処理をしてるわけですが、基本、こういう仕組みです。
boundsの原点がずれる事で、入れ子になったUIViewは相対的に移動するわけですな。
サンプルには、他にbasegroundViewのframe側を変更した場合とか、transformを変更した場合とかも-viewDidAppear:メソッドに書き込んでます。
を2や3にして、試してみてください。
transformを使ったやつが、ズームですな。
てなわけで、すなおにUIScrollView使わずに脱線してみました。
次回はちゃんとUIScrollViewを使ってのサムネイル画面。
------------
サンプルプロジェクト:puzzler-2.zip
ということで、始まり始まり~。
前回、サムネイルが入りきらない場合スクロールさせるって書いたけど、これには、UIScrollViewを使うわけです。
なんですが、せっかくなんでUIScrollView使う前に、UIViewでUIScrollViewっぽいことやってみます。UIScrollViewってのは、UIViewの入れ子機能やframe、boundsの機能をうまく使って実現してるっぽいんで、ここらへんを自分でやってみて原理を勉強しちゃおうってわけですわ。
で、まずpuzzlerAppDelegateでノーマルのUIViewControllerを作ってたのをやめて、サムネイル表示用のカスタムUIViewControllerを使うところから。
puzzlerAppDelegate.m内に書いてもいいんだけど、どのみち単体のファイルにするつもりなんで、ここで作っておきます。
というわけで、前回のpuzzler.xcodeprojを立ち上げて、いつものようにFile→New→New Fileメニューで出てくる画面でUIViewControllerテンプレートを選択~。
次に出てくる画面でSubclass ofをUIViewControllerにして~
XIBは作らないのでwith XIB for user interfaceのチェックも外す
名前をThumbnailViewController.mにして保存。
保存先はpuzzler/puzzler/フォルダ内あたりが妥当
ばっちりpuzzler.xcodeprojにThumbnailViewController.m/hが加わったら
puzzlerAppDelegate.mを変更だぜ!
といってもThumbnailViewController.hをimportして[UIViewController alloc]を[ThumbnailViewController alloc]に変えるだけ。
#import "puzzlerAppDelegate.h"
#import "ThumbnailViewController.h"
@implementation puzzlerAppDelegate
@synthesize window=_window;
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
・・・
UIViewController* receivedThumbController
= [[[ThumbnailViewController alloc]init] autorelease];
・・・
UIViewController* privateThumbController
= [[[ThumbnailViewController alloc]init] autorelease];
・・・
}
#import "ThumbnailViewController.h"
@implementation puzzlerAppDelegate
@synthesize window=_window;
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
・・・
UIViewController* receivedThumbController
= [[[ThumbnailViewController alloc]init] autorelease];
・・・
UIViewController* privateThumbController
= [[[ThumbnailViewController alloc]init] autorelease];
・・・
}
これで、ThumbnailViewControllerが使われることになるんで、あとはThumbnailViewController側のviewにサムネイル用のUIViewを入れ子にして貼付けていく。
receivedThumbControllerやprivateThumbControllerの型がUIViewController*にままだけど、特に変える必要はないです。ThumbnailViewControllerはUIViewControllerを継承してるんで、なんの間違いもないわけですな。
あとはThumbnailViewController側で、viewに子供のUIViewを追加していく作業。
ちょっと試しに真っ黒いビューを1個追加します。
余談だけど、この時点でかなりのビューがありますな。
うそ~ん、と思う人は、今回のサンプルのpuzzlerAppDelegate.m、-application:didFinishLaunchingWithOptions:メソッドの最後にある
// [self performSelector:@selector(test) withObject:nil afterDelay:1];
の注釈を外してみましょう。
この追加する黒いビューは実験用です。サムネイルビューを乗せていろいろ実験するので、ThumbnailViewControllerのインスタンス変数にします。
@interface ThumbnailViewController : UIViewController {
UIView* basegroundView; // サムネイルを乗せるビュー
}
@end
UIView* basegroundView; // サムネイルを乗せるビュー
}
@end
で、viewにbasegroundViewを追加するタイミングなんですが、-initWithNibName:bundle:メソッドではやらないように。
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
basegroundView = [[[UIView alloc] initWithFrame:CGRectMake(60, 100, 200, 200)]
autorelease];
basegroundView.backgroundColor = [UIColor blackColor];
[self.view addSubview:basegroundView];
}
return self;
}
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
basegroundView = [[[UIView alloc] initWithFrame:CGRectMake(60, 100, 200, 200)]
autorelease];
basegroundView.backgroundColor = [UIColor blackColor];
[self.view addSubview:basegroundView];
}
return self;
}
こんなふうになー
なんでかっていうと、UIViewControllerてのは、メモリが厳しくなってきて、その時に自分のviewが画面上に表示されていなければ、viewを解放しちゃうんですな。
試すのは簡単で、アプリ起動してハードウェア→メモリ警告をシミュレートメニュー選ぶだけ。
何にも起きねーじゃんと思うだろうけど、それは「自分のviewが画に表示されていなければ」という条件にあてはまらないから。あてはまるのは、見えてない方。今回なら「自分のもの」タブ側ってことです。「自分のもの」タブに切り替えて見ると~
ハイ消えたー、ドン
ちゅーわけで、青い背景色のビューは白い背景色のビューに変わっちゃてるし、basegroundViewも消えちゃうわけです。この状態でもう一回、ハードウェア→メモリ警告をシミュレートメニュー選んで、「貰い物」タブに切り替えると、赤い方もきっちり真っ白になります。
こんなふうに、UIViewControllerはメモリ厳しくなると「自分のviewが画面上に表示されていなければ」自分のview解放しちゃって、次に自分のviewが必要になると、あらためて新しく作るって事をします。
ゆく河の流れは絶えずして、しかももとの水にあらず
そもそもUIViewControllerの役割りはそういうもんで、UIViewのマネージャみたいなもんなわけです。- touchesBegan: withEvent:メソッドとかがあるんで、UIViewの一種だと思ってた人は、悔い改めるように。ぜんぜん違うから。
そういう視点でいうと、puzzlerAppDelegate.mの- application:didFinishLaunchingWithOptionsメソッドでやってるreceivedThumbController.viewに背景色を指定してるのも大間違いではあるんですが、いちいちプロパティ用意するのが面倒だったんでね。承知でやってるって事でよろしく。
まあ、そういうわけで、viewにbasegroundViewを追加するタイミングは-initWithNibName:bundle:メソッドでは駄目って事になります。
じゃ、どうするかというと、UIViewControllerは、自分のviewを作った直後に-viewDidLoadメソッドを呼ぶようになっているんで、そこで追加します。
- (void)viewDidLoad
{
[super viewDidLoad];
basegroundView = [[[UIView alloc] initWithFrame:CGRectMake(60, 100, 200, 200)]
autorelease];
basegroundView.backgroundColor = [UIColor blackColor];
[self.view addSubview:basegroundView];
}
{
[super viewDidLoad];
basegroundView = [[[UIView alloc] initWithFrame:CGRectMake(60, 100, 200, 200)]
autorelease];
basegroundView.backgroundColor = [UIColor blackColor];
[self.view addSubview:basegroundView];
}
こっちは、viewがあらためて作り直されるたびに呼ばれるので、何回、ハードウェア→メモリ警告をシミュレートメニュー呼ばれても問題ないです。ま、puzzlerAppDelegateでやった背景色は解除されちゃうけどな。というか解除ではなくviewそのものが別物になっとるわけです。
そこらへん気になる人は、ThumbnailViewControllerクラスに@propertyで背景色設定できるようにして、-viewDidLoadメソッドで、その背景色をviewに設定するようにすればいい。@property?な人は「iPhoneアプリ開発、号外 propertyをちょっと調べてみた」を読みましょう。ただし結構適当な内容、すまそ。
前に貼付けたbasegroundViewがどうなるかというと、前のviewが解放される時点で一緒に解放されます。autoreleaseやaddSubvewの後にreleaseってしていない場合は、メモリ上に残ってしまうので注意。特に実際にサムネイル画像使ってる場合、basegroundViewにはUIImageを持ったUIImageViewをたっぷりと貼る事になるんで、それがまるまるメモリ上に残る事になる。
それは、具合が悪いんでaddSubview:でself.viewにオーナーシップを渡したら、自分の方は管理を放棄してるわけです。
オーナーシップ?な人は日本語訳サイトの「Cocoaメモリ管理プログラミングガイド」を読みましょう。「iOSデバッグ&最適化技法 for iPad/iPhone」でも可!
ま、解放された場合、次に画面上に表示する時はビューの再構築が発生、複雑な構成であればあるほど、作りなおすのに時間がかかる、ユーザーはイライラするってわけで、これはメモリを取るか、スピードを取るかって話なわけですな。
話を元に戻そう。
このbasegroundViewに入れ子状でUIViewを貼付けるとどうなるかってのが、今日の本題なわけです。
- (void)viewDidLoad
{
・・・
[self.view addSubview:basegroundView];
CGRect r = CGRectMake(0, 0, 44, 44);
for (int i = 0; i < 100; i++) {
UIView* view = [[UIView alloc]initWithFrame:CGRectInset(r, 4, 4)];
view.backgroundColor = [UIColor colorWithHue:(float)i / 100.0
saturation:1
brightness:1
alpha:1];
[basegroundView addSubview:view];
r = CGRectOffset(r, r.size.width, 0);
if (r.origin.x > basegroundView.bounds.size.width) {
r.origin.x = 0;
r = CGRectOffset(r, 0, r.size.height);
}
}
}
{
・・・
[self.view addSubview:basegroundView];
CGRect r = CGRectMake(0, 0, 44, 44);
for (int i = 0; i < 100; i++) {
UIView* view = [[UIView alloc]initWithFrame:CGRectInset(r, 4, 4)];
view.backgroundColor = [UIColor colorWithHue:(float)i / 100.0
saturation:1
brightness:1
alpha:1];
[basegroundView addSubview:view];
r = CGRectOffset(r, r.size.width, 0);
if (r.origin.x > basegroundView.bounds.size.width) {
r.origin.x = 0;
r = CGRectOffset(r, 0, r.size.height);
}
}
}
実行すると、こんな感じ。
わざと、basegroundViewからはみ出した分も表示させてる
で、ここで入れ子になったUIVewiは、親側のUIViewのbounds座標形に従うって特性を利用して、basegroundView.boundsの原点をずらすとどうなるかってのが今回の実験です。
basegroundView.bounds = CGRectOffset(basegroundView.bounds, 20, 500);
て感じで、原点を500ポイントほどずらすとどうなるかってことですよ。見た目を劇的にしたかったんで、アニメーションでやってみます。
でもって、タブ切り替えるたびに繰り返すように、viewが表示された時に呼ばれる-viewDidAppear:メソッドでやってみます。
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
basegroundView.bounds = CGRectOffset(basegroundView.bounds,
0, -basegroundView.bounds.origin.y);
[UIView animateWithDuration:2 animations:^(void) {
basegroundView.bounds = CGRectOffset(basegroundView.bounds, 0, 500);
}];
}
{
[super viewDidAppear:animated];
basegroundView.bounds = CGRectOffset(basegroundView.bounds,
0, -basegroundView.bounds.origin.y);
[UIView animateWithDuration:2 animations:^(void) {
basegroundView.bounds = CGRectOffset(basegroundView.bounds, 0, 500);
}];
}
-viewDidLoadメソッドは、viewが作成された直後、-viewDidAppear:はviewが画面に現れた直後って違いを忘れないように。
-animateWithDuration:animationsはブロック構文版のbeginAnimation~commitAnimationsです。ブロック構文はその(227)、beginAnimation~commitAnimationsはその(37)あたりを見てね。commitAnimationsでブログ内検索すると、いろいろなところでヒットもする。
実行すると、タブ切り替えるたびにbasegroundViewに貼付けたUIViewが上にスクロールする。
いかにもbasegroundViewの内部が動いてるようにしたい人はbasegroundView.clipsToBoundsをYESにしましょう。
- (void)viewDidLoad
{
・・・
basegroundView.backgroundColor = [UIColor blackColor];
basegroundView.clipsToBounds = YES;
・・・
}
{
・・・
basegroundView.backgroundColor = [UIColor blackColor];
basegroundView.clipsToBounds = YES;
・・・
}
でもって、UIScrollViewみたいにフリックに対応して慣性スクロールしてるっぽくしたいならUISwipeGestureRecognizerを使って、上下スワイプジェスチャー見張って、スワイプ発見したらさっきのアニメーションを向きにあわせてやっちゃうわけです。
- (void)viewDidLoad
{
・・・
UISwipeGestureRecognizer* swipeUpGestureRecognizer
= [[[UISwipeGestureRecognizer alloc] initWithTarget:self
action:@selector(scroll:)] autorelease];
swipeUpGestureRecognizer.direction = UISwipeGestureRecognizerDirectionUp;
[basegroundView addGestureRecognizer:swipeUpGestureRecognizer];
UISwipeGestureRecognizer* swipeDownGestureRecognizer
= [[[UISwipeGestureRecognizer alloc] initWithTarget:self
action:@selector(scroll:)] autorelease];
swipeDownGestureRecognizer.direction = UISwipeGestureRecognizerDirectionDown;
[basegroundView addGestureRecognizer:swipeDownGestureRecognizer];
}
- (void)scroll:(UISwipeGestureRecognizer*)swipeGestureRecognizer
{
UISwipeGestureRecognizerDirection direction = swipeGestureRecognizer.direction;
float scroll_delta = 100;
if (direction == UISwipeGestureRecognizerDirectionDown) {
scroll_delta = -100;
}
[UIView animateWithDuration:0.5 animations:^(void) {
basegroundView.bounds = CGRectOffset(basegroundView.bounds, 0, scroll_delta);
}];
}
{
・・・
UISwipeGestureRecognizer* swipeUpGestureRecognizer
= [[[UISwipeGestureRecognizer alloc] initWithTarget:self
action:@selector(scroll:)] autorelease];
swipeUpGestureRecognizer.direction = UISwipeGestureRecognizerDirectionUp;
[basegroundView addGestureRecognizer:swipeUpGestureRecognizer];
UISwipeGestureRecognizer* swipeDownGestureRecognizer
= [[[UISwipeGestureRecognizer alloc] initWithTarget:self
action:@selector(scroll:)] autorelease];
swipeDownGestureRecognizer.direction = UISwipeGestureRecognizerDirectionDown;
[basegroundView addGestureRecognizer:swipeDownGestureRecognizer];
}
- (void)scroll:(UISwipeGestureRecognizer*)swipeGestureRecognizer
{
UISwipeGestureRecognizerDirection direction = swipeGestureRecognizer.direction;
float scroll_delta = 100;
if (direction == UISwipeGestureRecognizerDirectionDown) {
scroll_delta = -100;
}
[UIView animateWithDuration:0.5 animations:^(void) {
basegroundView.bounds = CGRectOffset(basegroundView.bounds, 0, scroll_delta);
}];
}
ここで使ってるUISwipeGestureRecognizerてのは、ユーザーのスワイプジェスチャーを察知したらinitWithTarget:で指定したオブジェクトのaction:で指定したメソッドを呼んでくれるってクラスです。
実際のUIScrollViewは、もっときめ細かい処理をしてるわけですが、基本、こういう仕組みです。
boundsの原点がずれる事で、入れ子になったUIViewは相対的に移動するわけですな。
サンプルには、他にbasegroundViewのframe側を変更した場合とか、transformを変更した場合とかも-viewDidAppear:メソッドに書き込んでます。
#define ANIMATION_MODE 1
を2や3にして、試してみてください。
transformを使ったやつが、ズームですな。
てなわけで、すなおにUIScrollView使わずに脱線してみました。
次回はちゃんとUIScrollViewを使ってのサムネイル画面。
------------
サンプルプロジェクト:puzzler-2.zip