2011年8月12日金曜日

[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の使い勝手もアップ!ですね。

2011年8月9日火曜日

[UITableViewController scrollToRowAtIndexPath:atScrollPosition:animated:] の挙動まとめ

UITableViewController の scrollToRowAtIndexPath:atScrollPosition:animated: メソッドは、対象のテーブルビューのセクションにヘッダ・フッタが付いている場合挙動が変化する事がわかったので、ちょっと調査してまとめてみました。具体的には以下のような動きをするようです。

  • このメソッドは自分で呼び出すか、またはテーブルビューのセルの中に UITextField のようなフォーカスを取るコントロールを配置して、それが選択されたときに呼び出される
  • このメソッドで指定した indexPath の section に Header View or Header Text / Footer View or Footer Text が指定されているとき、このメソッドは選択された indexPath の row だけではなく、それらのヘッダやフッタも同時に表示される位置にスクロールしようとする
  • ということであんまり長い Section Header / Section Footer を作ると scrollToRowAtIndexPath:atScrollPosition:animated: の挙動がおかしくなる
  • Table View Header / Table View Footer については全然無関係なので長くしても大丈夫
画像にすると以下のような感じになります。

初期状態

section3つ、row3つ、合計9行のtable viewを作って、それぞれにsection header / section footerを追加しました。このテーブルビューを使って実験を行います。

UITableViewScrollPositionTopを指定してスクロール
section0, row0section0, row1
section0, row2
section1, row0

UITableViewScrollPositionTopを指定すると、sectionの一番上のrowが指定された場合のみ、そのsectionのsection headerの高さを考慮してスクロールするようになります。

UITableViewScrollPositionMiddleを指定してスクロール
section0, row0section0, row1section0, row2
section1, row0section1, row1section1, row2

UITableViewScrollPositionMiddleの場合は特にsection header / section footer関係なく、中央に選択された行が来るようにスクロールするようです。

UITableViewScrollPositionBottomを指定してスクロール
section0, row0section0, row1section0, row2
section1, row0section1, row1
section1, row2

UITableViewScrollPositionBottomを指定すると、sectionの一番下のrowが指定された場合のみ、そのsectionのsection footerの高さを考慮してスクロールするようになります。