頭と尻尾はくれてやる!

パソコンおやじのiPhoneアプリ・サイト作成・運営日記


id型のオブジェクトのプロパティへアクセスできない?

似たようなクラスが複数あってそのどれかへ同じメッセージを送りたい場合に困ったことがあったのでメモ。

例えばViewController1とViewController2ってクラスがあって、それらのオブジェクトがviewController1とviewController2とするよ。どちらのクラスにもhoge、fooなんてプロパティがあるんだ。
{
    if (flag) {
        viewController1.hoge = hoge;
        viewController1.foo = foo;
    } else {
        viewController2. hoge = hoge;
        viewController2.foo = foo;
    }
}
↑このように条件によって送り先は違うんだけど、同じプロパティにアクセスして同じ値を設定したい場合。
ここでは二つのプロパティしか書いてないけどこれがたくさんあったらなんだか無駄と思うじゃない?
すると当然先に参照を得ておいて、それからプロパティのセットをしてやろうってなるよね。

それで書き直したのがこれ↓
{
    id targetViewController;
    if (flag) {
        targetViewController = viewController1;
    } else {
        targetViewController = viewController2;
    }

    targetViewController.hoge = hoge;
    targetViewController.foo = foo;

}
このようにid型でtargetViewControllerで参照を得ておけばいいよね、と思ったんだけどこれだとXcodeはエラーをだしてビルドしてくれないんだ。

Property not found on object of type 'id'
Property 'hoge' not found on object of type 'id'

id型のオブジェクトにはhogeなんてプロパティはございませぬ、だと、、、?
バカヤロー、なんのためにidでやってると思ってんだよ!って言いたくなるけどXcodeにいくら言ってもビルドしてくれないんじゃしょうがない、ここは書き直して、、、
{
    [targetViewController setHoge:hoge];
    [targetViewController setFoo:foo];
}
このようにドット記法をやめてセッターでプロパティへアクセスするとエラーじゃなくなる。そうなのー?って気がしないでもないんだけど、そういうものみたい。


返り値の型が一致してないだと?

なんで???ってエラーにぶつかって無駄な時間を使っちゃったのでメモしておくよ。

こういう形のブロックを返すクラスメソッドがあったんだ。
typedef id (^ HogeBlock)(int number);//Blockの定義


+(HogeBlock)getHogeBlock
{
    return (id)^(int number) {

        //引数に応じた処理

        NSMutableData *data = …
        return data;
    };
}
ところがどういうわけか最後のreturn部分を指してエラーがあるとXcodeが言うんだ。

エラーメッセージ return type must match
Return type 'NSMutableData *' must match previous return type 'void *' when block literal has unspecified explicit return type

なんでこれでエラー出るんだよ?って散々悩んでいたんだけどさ、ようやくわかったのが
//引数に応じた処理
って部分にエラーチェック用で
if (something) return nil;
みたいな部分があったんだ。これが原因。
そうわかるとエラーメッセージが何を言ってるのかわかったよ。
ブロック構文において明確な返り値を記述しない場合、この返り値の型'NSMutableData *'は前述の型'void *'と一致しないとだめだよ!
ってことか。previousがブロックの宣言文の形式のことかと思ってたからXcodeは何を言ってるんだよ?ってはまってたんだな。

idだからなにを返してもいいってわけではなくて、返り値の型が全部同じかチェックしてて、一致しないと最初の方には何も記述なしで、二つ目以降にエラーって出るんだ。 どうせならその最初の部分もはっきりと示してくれればいいのに!って言っても仕方ないか。これからは気をつけようっと。


if (something) return (NSMutableData *)nil;
↑エラー自体はキャストしてやれば回避できるけど、、、ダサっ!
そもそも返り値を持つ関数などで途中でぷいっと処理を中断して戻る、なんて記述しちゃだめなのかな。
後からチェックする時に途中にさりげなくreturn文なんかがあると理解しにくくなるとか。
NSMutableData *data = nil;
if (something) {
    //trueの場合の処理
} 
return data;
↑こういう形にしてreturnは一つだけってコーディングルールにしておく方が理解しやすいかも。

みんなどうしてるんだろうな、と思って調べると、、、ずばりこんのがあったよ!

関数の途中でreturnはしてはいけない? - Yahoo!知恵袋

なるほど、returnは一つだけってルールにすると最後まで読まないとだめ、か、、、確かにそうだね。
下の方の回答に処理が終わってることを//------------------> のようなコメントで明記するってのがあるけど、こういうのがいいかも。


ちなみに、ブロック構文じゃなくて普通のメソッドの返り値がid型だと
-(id)testReturnValue
{
    if (1) return nil;
    
    return @"hoge";
}
なんて記述してもエラーは出ない。そうかあ、ブロック構文だとちょっと違うんだねえ。


iPhoneのタブバーをフラットデザインっぽく変えてみる

頭と尻尾はくれてやる! / ナビゲーションバーの色(png画像)を即時変える
↑ここでボタンをタップしたらナビゲーションバーの見た目を変えたので、今度はタブバーも同じようにやってみるよ。

iPhoneのタブバーのサイズは320x49(つまりRetina用だと640x98ピクセルの画像が必要)。
タブバーの見た目を変えるために必要な画像はこんな感じ。
(1)320x49全体の背景の画像(非選択時のタブはこれが見える)
(2)320/n x 49の選択されてるタブの背景画像(n個のタブがあるってことで幅は320÷n)
(3)アイコン画像(選択時と非選択時の両方)

一応フラットデザインっぽいのを狙っているので(1)と(2)はコードでpng画像を生成してみた。
画像生成部分はすでにやったので置いといてタブバーに適用する部分はこんな感じ。
{
    //(1)320x49全体の背景の画像
    [[UITabBar appearance] setBackgroundImage:tabBarImage];
    [self.navigationController.tabBarController.tabBar setBackgroundImage: tabBarImage];

    //(2)選択されてるタブの背景画像
    [[UITabBar appearance] setSelectionIndicatorImage:oneTabImage];
    [self.navigationController.tabBarController.tabBar setSelectionIndicatorImage:oneTabImage];

    //(3)アイコン画像
    UITabBar *tabBar = self.tabBarController.tabBar;
    
    UITabBarItem *item0 = [tabBar.items objectAtIndex:0]; //1番左のタブが0
    [item0 setFinishedSelectedImage:selectedIconImage0 withFinishedUnselectedImage: unselectedIconImage0];

    UITabBarItem *item1 = [tabBar.items objectAtIndex:1];
    [item1 setFinishedSelectedImage:selectedIconImage1 withFinishedUnselectedImage: unselectedIconImage1];

    UITabBarItem *item2 = [tabBar.items objectAtIndex:2];
    [item2 setFinishedSelectedImage:selectedIconImage2 withFinishedUnselectedImage: unselectedIconImage2];

    UITabBarItem *item3 = [tabBar.items objectAtIndex:3];
    [item3 setFinishedSelectedImage:selectedIconImage3 withFinishedUnselectedImage: unselectedIconImage3];
}
(1)と(2)でそれぞれ二行あるのはappearanceを使った場合だと、現在表示しているビューのに対しては反映してくれないからってのもナビゲーションバーの時と同じ。

ってことで「赤ちゃんの成長グラフ」アプリでやってみたんだ。

変更前

↑テスト用タブを追加してそこでボタンをタップすると、、、


変更直後

↑タップした画面がタップした瞬間こんな感じに。


変更後の別タブ

↑別のタブへ行くとこんなのに。ふんふん、意図通りには動いてるみたいだな。


iPadを横向きで起動するとself.view.boundsが意図通りに取れない

頭と尻尾はくれてやる! 回転させたらframeのwidthがなんか違う?
↑ここで画面のサイズを取得するのにself.view.frameじゃなくてself.view.boundsを使えばいいじゃんってしてるんだけど、iPadの場合だとちょいと具合が悪いんだよね。

前にやったのと同じことをiPadでやってみるとこんな感じ(画像のサイズが若干違うけどね)。ステータスバーの有無による挙動はiPhoneの場合と同じなのでここは無しの場合だけってことで。

(1)Portraitの場合
iPad, Portrait
frame x=0 , y=0 , width=768 , height=1024
bound x=0 , y=0 , width=768 , height=1024

(2)Landscapeの場合
iPad, Landscape
frame x=0 , y=0 , width=768 , height=1024
bound x=0 , y=0 , width=1024 , height=768

Landscapeでのプロパティを見るとframeのwidthよりheightの方が大きいことがわかるよね。
ここまではいいんだよ。iPhoneの場合と同じだし。

ところがiPadの場合、iPhoneと違って横向き(Landscape)で起動できるもんだから起動直後の画面でself.view.frameとboundsを取得するとこうなるんだ。

iPad, Landscape, fixed
frame x=0 , y=0 , width=768 , height=1024
bound x=0 , y=0 , width=768 , height=1024

↑げっ!横長なのにboundsプロパティも"縦長"になってる。

iPhoneの場合はアプリのボタンを押すときはPortraitの状態になってるのでiPhoneを横向きにしてアプリをスタートさせると
willAnimateRotationToInterfaceOrientation:duration:
メソッドがコールされるのでこの後ならboundsプロパティは期待通り"横長"になってくれる。
ところが、iPadの場合に横向きでアプリをスタートさせてもこのwillAnimateRotationToInterfaceOrientation:duration:メソッドはコールされないんだよね。

それじゃあどうすんの?ってことで調べると起動時のデバイスの方向を得ることができるんだってさ。
{
    UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation];
    if (orientation == UIInterfaceOrientationPortrait) {
        NSLog(@"縦長");
    } else {
        NSLog(@"横長");
    }
}
statusBarOrientationプロパティで得ることができるのか、、、まさかと思ってステータスバーを非表示で動作確認してみたけどちゃんと取得できたよ。これでなんとかなりそうだね。


ナビゲーションバーの色(png画像)を即時変える

フラットデザインっぽいナビゲーションバーってどんな感じかな?
と思って、いろいろとナビゲーションバーの色を変えようとしたんだ。

頭と尻尾はくれてやる! ナビゲーションバーをフラットデザインっぽくしてみる
↑に書いたようにtintColorで色を設定してもグラデーションがかかってフラットデザインっぽくないのでpng画像が欲しい、と。ところがいちいちイラストレータやiDrawみたいなツールで作るのも面倒なのでコードでなんとかならないのん?と思ってさ。
いろいろとやり方はあるんだろうけど、とりあえずコードでpng画像を実際に作成してそれを適用させるみたいなことをやってみたんだよ。
ほら、アプリの雰囲気を変えるのにカラーテーマを選択したらあっちこっちの色が変わる、とかそういうのね。

それじゃ、ボタンをタップしたらpng画像を作るってところの基本的な流れ。
{
    UIView *view = [[[UIView alloc] initWithFrame:CGRectMake(0, 0,size.width, size.height)] autorelease];
    view.backgroundColor = color;

    UIGraphicsBeginImageContextWithOptions(size,NO, 0);

    CGContextRef context = UIGraphicsGetCurrentContext();
    [view.layer renderInContext:context];
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    
    NSData *data = UIImagePNGRepresentation(image);

    if ([data writeToFile:filePath atomically:YES]) {
        NSLog(@"OK");
    } else {
        NSLog(@"Error");
    }
}
ここでsizeは作りたい画像のサイズ。iPhoneのナビゲーションバーなら通常320x44だよね(Retinaでもこのサイズ)。colorは作りたい画像の色を示すUIColorオブジェクト。
filePathは保存する場所。

前からスクリーンショット撮影なんかでこの辺りの処理は使っていたんだけど、今回「そうなんだ?」って思ったのがpng画像の元となるviewってオブジェクトを別にどこかへaddSubview:する必要はないってこと。上のコードでもしてないでしょ?
どこかへaddSubview:して実際に描画したところに対してrenderInContext:してやらないとだめって漠然と思っていただけに意外。

ともかく、次にこうやって作った画像をすぐに適用させなくちゃいけないよね。

さっきの続き。
{
    UIImage *barImage = [UIImage imageWithContentsOfFile: filePath];
    [[UINavigationBar appearance] setBackgroundImage: barImage forBarMetrics:UIBarMetricsDefault];
}
実はファイル名がわかってて、そこからUIImageオブジェクトを作るのってどうするんだろう?って思ったんだ。 だって普段imageNamed:@ファイル名 みたいなのしか使ってなかったからさ。
調べたらちゃんとあるんだね(そりゃそうだろう)。imageWithContentsOfFile:メソッドなんてのがあるんだ。
そうやって作ったUIImageオブジェクトを適用させるってのが上のコードなんだけど、これだと具合がちょっと悪いんだよね。
というのもユーザーがボタンをタップして、その処理を記述してるところに上のコードを書いても見ている画面のナビゲーションバーには反映されないんだ。画面を遷移させるとOKなんだけどさ。
これって微妙でしょ?
できることなら、このカラーテーマ!ってボタンをタップしたらすぐに切り替わって欲しいじゃない。
{
    [self.navigationController.navigationBar setBackgroundImage: barImage forBarMetrics:UIBarMetricsDefault];
}
↑そういうことで追加。これならボタンタップした瞬間に色が変わる。二度手間って気がしないでもないけど仕方ないよね。







Copyright ©頭と尻尾はくれてやる!. Powered by FC2 Blog. Template by eriraha.