landscapeの判定には[UIApplication sharedApplication].statusBarOrientationを使う


デバイスの向きがポートレイトなのかランドスケープなのかの判定をしなければならない場合がある。例えば、キーボードの高さを取得するときだ。viewWillAppear:などでキーボード表示のノーティフィケーションを登録して、下記の場合keyboardWasShownでキャッチする。
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(keyboardWasShown:)
name:UIKeyboardWillShowNotification object:nil];
そうして
– (void)keyboardWasShown:(NSNotification*)aNotification
{
NSDictionary* info = [aNotification userInfo];
CGSize kbSize = [[info objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size;
のようにしてキーボードのサイズをとる。ここまでは、Appleのサイトの以下のところに書かれている
Text, Web, and Editing Programming Guide for iOS: Managing the Keyboard
ところが、実際にはキーボードの高さはポートレイトの時には kbSize.height でよいのだが、ランドスケープの時には kbSize.width の方がキーボードの高さになるのだ。

そこで、デバイスの向きを判定することが必要になる。
僕は当初、UIDeviceOrientation orientation = [[UIDevice currentDevice] orientation];をつかった。つまり
UIDeviceOrientation orientation = [[UIDevice currentDevice] orientation];
if (orientation == UIDeviceOrientationPortrait || orientation == UIDeviceOrientationPortraitUpsideDown ) {
keyboardHeight_ = kbSize.height;
} else {
keyboardHeight_ = kbSize.width;
}
ということになる。ところが、これはまずかった。
なぜかというと、iPhoneがdidRotateFromInterfaceOrientationを判定するのにはある程度余裕が持たせてあって、ちょっとした傾きでは回転しないようになっているが、[UIDevice currentDevice]はすごくシビアに判定してしまうからだ。
例えば、机の上にほぼ平行に置いたiPhoneは表示上はポートレイト状態なのに、実際にはほんのわずかに傾いていて[UIDevice currentDevice]では横向きであると判定してしまうことがあるのである。
この問題をどうすべきか悩んでいたら、どんぴしゃのまさに回答そのものを以下のページで見つけることができた。
objective c – UIKeyBoard resize on orientation change to landscape – Stack Overflow
向きの判定には[UIApplication sharedApplication].statusBarOrientationを使えばよかったのだ。
switch ([UIApplication sharedApplication].statusBarOrientation) {
case UIInterfaceOrientationPortrait:
case UIInterfaceOrientationPortraitUpsideDown:
keyboardHeight_ = kbSize.height;
break;
case UIInterfaceOrientationLandscapeLeft:
case UIInterfaceOrientationLandscapeRight:
keyboardHeight_ = kbSize.width;
break;
}
となる。

スクリーンサイズの取得はviewWillAppearですべきと今更ながら知る


ViewDidLoadでself.view.frameを取得したら、ランドスケープの時widthとheightが逆なので、いちいち自分で入れ替えをしてステータスバー分を計算に入れ、portrateSize_ と landscapeSize_ というCGSizeを作って使い回していた。

下記サイトのによるとViewDidLoadではなくviewWillAppearで取得すべきなのだという。初歩的なことだったろうが僕は知らなかった。
uiview – “Incorrect” frame / window size after re-orientation in iPhone or iPad – Stack Overflow

以下のコードで試してみた。
– (void)viewDidLoad
{
NSLog(@”ViewDidLoad:width/height:%f/%f”,self.view.frame.size.width, self.view.frame.size.height);
}

– (void)viewWillAppear:(BOOL)animated
{
NSLog(@”viewWillAppear:width/height:%f/%f”,self.view.frame.size.width, self.view.frame.size.height);
}

結果は、
ポートレイトのとき
ViewDidLoad:width/height: 320.000000/460.000000
viewWillAppear:width/height: 320.000000/372.000000

ランドスケープの時
ViewDidLoad:width/height: 300.000000/480.000000
viewWillAppear:width/height: 480.000000/236.000000

ツールバーの分もちゃんと組み込まれている。

知らないということは恐ろしいことだ。

CoreDataのTableViewでのmoveRowAtIndexPathの謎


CoreDataに連動したTableViewのアプリの場合、おなじみのfetchedResultsControllerの書いて、各レコードにはソート番号を入れるdisplayOrderというNSNumberの項目を作って表示順を管理している。
まず、第一の謎。ソート番号って必要なのか?

fetchedResultsControllerを用いないのであれば、ソートのためのdisplayOrderは作らずに、物理的な行の入れ替えだけを行えばいいようなのだが、CoreDataの場合、レコード生成した順番が必ずしも物理的な順番とは限らないので、やはりソート番号による管理は必須のようだ。

第二の謎。moveRowAtIndexPathに来た時点では、indexPathの順番は既に変わっているのだろうか。それとも、まだ変わっていないのか?つまり僕は、moveRowAtIndexPathはどこからどこへの移動するか教えてくれるが、TableView上での移動自体はこちらの仕事かもしれないと思ったわけである。

サンプルコードを読むと、TableView上では順番は既に変わっていると考えてよいようだ。ということはTableViewは編集済みと考えて、この編集結果に基づいてCoreData上のソート番号をリナンバーするだけでよい。

さていよいよ、ではmoveRowAtIndexPath で入れ替えたレコードのソート番号をどのように再編集すべきか。ググって出てくる下記のようないくつものサンプルコードある。
iphone – Rearranging UITableView with Core Data – Stack Overflow
Reordering rows in a UITableView with Core Data
Re-Ordering NSFetchedResultsController
How can I maintain display order in UITableView using Core Data?
やはりレコード全部のソート番号をリナンバするのではなく移動元の行から移動先の行までのレコードだけ調べてソート番号の番号を振り直しているのがかっこいい。

僕としては、一番下のものがしっくりくる。移動元と移動先のソート番号をスワップして、それ以外のものを一個ずつずらすという論理がわかりやすいからだ。

しかし、これらを見て第四の謎が生じる。
みんな新規レコードのセルはテーブルの先頭に追加しているのだろうか、それとも末尾に追加か?普通は先頭に追加ではないかと思う。そして同時に、レコード生成時にソート番号をどうやって生成しているのだろう。新しいレコードのソート番号を全ソート番号の最大値+1にするのではないかな。だとすればソート順は降順になる。

なのに上記のググったサンプルはことごとく昇順を基本にしている。不安である。ソート番号の生成方法が僕の場合間違っているかも。どうするのがいいか、わからないままとりあえず降順管理をすることにした。

その点で、一番下のサンプル例の場合、1)スワップする値はtoIndexそのものではないので工夫が必要ということと、2)設定しているdelta値のプラスマイナス値を逆にすればいいだけなので、そういう意味でもこのサンプルが気に入っている。

1)については以下のようにした。

FFObject *affectedObject = nil;
affectedObject = [self.fetchedResultsController.fetchedObjects objectAtIndex:toIndex];
NSNumber* toOrder = affectedObject.displayOrderValue;
affectedObject = [self.fetchedResultsController.fetchedObjects objectAtIndex:fromIndex];
affectedObject.displayOrderValue = toOrder;