2011年8月12日金曜日

UIPanGestureRecognizerはiOS4.0ではtranslationプロパティを正しく返さない

UIPanGestureRecognizerのtranslationプロパティは、iOS 4.0でかつUIScrollViewの配下になっているviewに対して取り付けた場合、常にCGPointZeroを返してしまうようです。iOS 4.1のシミュレータで確認したら直ってましたので、iOS 4.0限定だと思われます。というわけでvelocityプロパティをかわりに使っておくことをお進めします。

検証結果はこちら
// Assume "self" is added to a UIScrollView
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
NSLog(@"%s", __func__);
if ([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]]) {
// Ignore horizontal or vertical pans depending on the orientation property
UIPanGestureRecognizer *panGesture = (UIPanGestureRecognizer *)gestureRecognizer;
CGPoint translation = [panGesture translationInView:self];
NSLog(@" * self.frame = %@", NSStringFromCGRect(self.frame));
NSLog(@" * translation = %@", NSStringFromCGPoint(translation));
NSLog(@" * velocity = %@", NSStringFromCGPoint([panGesture velocityInView:self]));
NSLog(@" * location = %@", NSStringFromCGPoint([panGesture locationInView:self]));
NSLog(@" * superview.frame = %@", NSStringFromCGRect(self.superview.frame));
NSLog(@" * supertranslation = %@", NSStringFromCGPoint([panGesture translationInView:self.superview]));
NSLog(@" * supervelocity = %@", NSStringFromCGPoint([panGesture velocityInView:self.superview]));
NSLog(@" * superlocation = %@", NSStringFromCGPoint([panGesture locationInView:self.superview]));
switch (orientation)
{
case Holizontal:
{
return fabs(translation.x) > fabs(translation.y);
}
case Vertical:
{
return fabs(translation.x) < fabs(translation.y);
}
default:
{
NSAssert1(NO, @"orientation value %d is invalid.", orientation);
return NO;
}
}
}
return YES;
}
/*
Result in iOS 4.0.0
-[MyView gestureRecognizerShouldBegin:]
* self.frame = {{856, 0}, {107, 380}}
* translation = {0, 0}
* velocity = {-256.336, -427.227}
* location = {0, 218}
* superview.frame = {{0, 0}, {320, 380}}
* supertranslation = {0, 0}
* supervelocity = {-256.336, -427.227}
* superlocation = {856, 218}
-[MyView gestureRecognizerShouldBegin:]
* self.frame = {{856, 0}, {107, 380}}
* translation = {0, 0}
* velocity = {-126.199, 25.2398}
* location = {4, 196}
* superview.frame = {{0, 0}, {320, 380}}
* supertranslation = {0, 0}
* supervelocity = {-126.199, 25.2398}
* superlocation = {860, 196}
Result in iOS 4.3.4
-[MyView gestureRecognizerShouldBegin:]
* self.frame = {{749, 0}, {107, 380}}
* translation = {0, -6.5}
* velocity = {15.7758, -260.301}
* location = {58, 300.5}
* superview.frame = {{0, 0}, {320, 380}}
* supertranslation = {0, -6.5}
* supervelocity = {15.7758, -260.301}
* superlocation = {807, 300.5}
-[MyView gestureRecognizerShouldBegin:]
* self.frame = {{642, 0}, {107, 380}}
* translation = {8, 0}
* velocity = {283.102, 78.5224}
* location = {107.5, 122}
* superview.frame = {{0, 0}, {320, 380}}
* supertranslation = {8, 0}
* supervelocity = {283.102, 78.5224}
* superlocation = {749.5, 122}
*/
view raw gistfile1.m hosted with ❤ by GitHub

[NSObject load] と [NSObject initialize] の違い

クラスがObjective-Cのランタイムにロードされ利用可能になったタイミングで、そのクラス全体の初期化を行いたいということはよくあると思います。Objective-CではNSObjectクラスの以下のメソッドを用いてクラス全体の初期化を行うことができます。
  • + load
  • + initialize
この2つですが、結構挙動が異なります。詳細については以下のとおり。
http://cocoawithlove.com/2008/03/cocoa-application-startup.html
  • loadメソッドはクラスがロードされて利用可能になったら即座に呼び出される。
    • このとき、自分以外の他のクラスはまだロードされていない可能性があるので、自分以外のクラスを利用するような初期化はできない。
    • main関数の内部のNSAutoReleasePoolが用意されるよりも先に呼び出されるので、autoreleaseを使うような初期化を行う場合には自分でNSAutoReleasePoolを生成して管理する必要がある
  • initializeメソッドはそのクラスに実際のアクセスが最初に発生したタイミングで呼び出される。
    • 要するに一度も使われないクラスでは呼び出されない。
    • 自分以外のクラスもロードが完了しているので、自由に他のクラスを利用できる。
    • autoreleaseについても特に気にしなくて良い。
基本はinitializeメソッドを使うほうがより安全で確実なうえに、使われないなら初期化されないので経済的でいい感じです。こちらを使うことをお勧めします。

またloadメソッドについては、iOS実機で自家製frameworkを使っているを使っているとき、framework内部にビルドされているクラスのloadメソッドが呼び出されないという問題があります(静的ライブラリ.aについては未検証)iOSシミュレータおよびMacではきちんとframeworkに含まれているクラスについてもloadメソッドが呼び出されるのですが・・・ともかく地雷が大きいので避けたほうが懸命です。

[UIView willMoveToSuperview:] が便利です

UIKitやFoundationには、iOS 2.0のころから存在するのに、意外と知られていない便利なメソッドやプロパティがたくさんあります。今回はUIViewのメソッドをご紹介します。

UIViewはUIViewControllerと違ってライフサイクルが単純で、どのタイミングで自分自身が画面上に追加されたのか、どのタイミングで自分自身が画面から外されたのか、などを把握しづらいとお嘆きの方がいらっしゃると思います。事実その用途のためだけにUIViewControllerを使ってプログラミングをしている人も見かけます。そこで以下のメソッドをご紹介です。
  • willMoveToSuperview:
    • 自分自身が新しいSuperview以下に移動しようとしたとき(新しいSuperviewに対してaddSubview:されようとしたとき)に呼び出されます。
  • didMoveToSuperview
    • 自分自身が新しいSuperview以下に移動したとき(新しいSuperviewにaddSubview:されたとき)に呼び出されます。
  • willMoveToWindow:
    • 自分自身が新しいWindow以下に移動しようとしたとき(新しいWindowに対してaddSubview:されようとしたとき)に呼び出されます。
  • didMoveToWindow
    • 自分自身が新しいWindow以下に移動したとき(新しいWindowに対してaddSubview:されたとき)に呼び出されます。
  • didAddSubview:
    • 自分自身に他のviewがsubviewとして追加されたときに呼び出されます。
  • willRemoveSubview:
    • 自分自身のsubviewsから他のviewが取り除かれようとしているときに呼び出されます。
これらのメソッドをUIViewのサブクラスでオーバーライドすることにより、かなりの自由度でviewの動きをコントロールすることができます。
たとえば自作のUIViewで、画面にviewが追加されたタイミングで何かしたい・・・というときなどは以下のようにできます:
- (void)willMoveToSuperview:(UIView *)newSuperview

{
NSLog(@" * superview = %@", newSuperview);
NSLog(@" * superview's window = %@", newSuperview.window);
// UIViewControllerでいうところの loadView 兼 viewDidLoad 兼 viewWillAppear 兼 viewWillDisappearみたいなタイミング
}

- (void)didMoveToSuperview
{
// UIViewControllerでいうところの viewDidAppear 兼 viewDidDisappear みたいなタイミング
// ここで、もしsuperviewがあり(画面に表示される可能性があり)、まだ自分自身のデータが初期化されていない場合には
// reloadDataして初期表示データを読み込む
// superviewがない場合には画面から外されたのですべてのビューまわりをリセットして、次の表示に備えるようにしておく
if (self.superview) {
if (!self.someData) {
[self reloadData];
}
} else {
self.someData = nil;
[self __resetOutlets];
}
}

- (void)willMoveToWindow:(UIWindow *)newWindow
{
NSLog(@" * window = %@", newWindow);
// いまいち使いづらいのでwillMoveToSuperviewとかを使うようにしてます
}

- (void)didMoveToWindow
{
// いまいち使いづらいのでdidMoveToSuperviewを使うようにしてます
}
これでUIViewの使い勝手もアップ!ですね。