開発 | HMDT Blog | ページ 6

カテゴリー : 開発

Xcode 4のメソッド一覧で検索


最近、うちの会社の人から教えてもらった話。

Xcodeのエディタウインドウの上の方に、メソッド一覧をポップアップ表示してくれるボタンがあるじゃない。行きたいメソッドにすぐジャンプできるやつ。これは知ってた。

Xcode 4では、そのポップアップを表示した状態でテキストを入力すると、メソッド名の検索ができる!これは知らなかった!

メソッドがたくさんあるときに便利。最近作っているアプリは、1クラスに数百メソッドはざらなので、とっても便利。

iOS 5 GM遂に登場!


ということで、やっと出てきたiOS 5 GM。絶賛インストール中。

ユーザへの正式公開まで一週間しかないという、相変わらずのタイトスケジュール。今日は、ビルドと申請祭りだ!

UIScrollViewでズームを行ったときに常に対象を中央にする


難しいことを解決するには、手を付けやすいところから一歩ずつ。ということで、スクロールビューでズームを行ったときに、対象を常に中央に表示するサンプルコードを。

iOSでは、スクロール機能を提供するUIScrollViewがかなり便利。スクロールだけでなく、ズームも一緒に行う事ができる。ただ、UIScrollViewのズームは、そのまま使うと左上固定で拡大縮小されてしまう。これは違和感がある。画面の中心を基準に拡大縮小してほしいところ。

これをできるだけ簡単にやってみた。

ポイントは2つある。1つは、UIScrollViewのzooming viewとして、表示されているUIImageViewを使う。つまり、viewForZoomingInScrollView:の返り値として、UIImageViewのインスタンスを返す。これはまぁ、なんとなく分かるでしょ。で、気づかなかったのが、UIScrollViewはズーミングを行っているときは、zooming viewのサイズをcontentSizeとして使うらしい。つまり、zooming viewを使っている場合は、contentSizeを明示的に設定する必要は無い。つーか、設定したらまずいかも。

2つめは、UIImageViewを常に中央に表示するために、UIImageViewのframeのoriginを変更する。UIScrollViewのcontentOffsetじゃないよ。あくまでUIImageViewのframeを直接動かす。ただし、このときframeのsizeを変更しては行けない。sizeはUIScrollViewが変更してくれるので、それを使う。

ということで、それを実現してみたのが、次のコード。まずは初期化処理。UIScrollViewとUIImageViewを作成する。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // Create window
    self.window = [[[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]] autorelease];

    // Create scroll view
    _scrollView = [[UIScrollView alloc] initWithFrame:[UIScreen mainScreen].applicationFrame];
    _scrollView.maximumZoomScale = 4.0f;
    _scrollView.delegate = self;
    [self.window addSubview:_scrollView];

    // Create image view
    _imageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"test.png"]];
    [_scrollView addSubview:_imageView];

    // Update image view
    [self _updateImageViewSize];
    [self _updateImageViewOrigin];

    // Show window
    self.window.backgroundColor = [UIColor whiteColor];
    [self.window makeKeyAndVisible];

    return YES;
}

UIScrollViewをインスタンス化して設定。UIImageViewをインスタンス化して設定。そして、UIImageViewのサイズと座標を設定する。それぞれ_updateImvewViewSizeと_updateImageOriginというメソッドを用意した。_updateImageViewSizeは、UIImageViewを縦幅いっぱいまたは横幅いっぱいまで広げるためのもの。_updateImageOriginは、それを画面中央に表示するために移動するためのものだ。

- (void)_updateImageViewSize
{
    // Get image size
    CGSize  imageSize;
    imageSize = _imageView.image.size;

    // Decide image view size
    CGRect  bounds;
    CGRect  rect;
    bounds = _scrollView.bounds;
    rect.origin = CGPointZero;
    if (imageSize.width / imageSize.height > CGRectGetWidth(bounds) / CGRectGetHeight(bounds)) {
        rect.size.width = CGRectGetWidth(bounds);
        rect.size.height = floor(imageSize.height / imageSize.width * CGRectGetWidth(rect));
    }
    else {
        rect.size.height = CGRectGetHeight(bounds);
        rect.size.width = imageSize.width / imageSize.height * CGRectGetHeight(rect);
    }

    // Set image view frame
    _imageView.frame = rect;
}

- (void)_updateImageViewOrigin
{
    // Get image view frame
    CGRect  rect;
    rect = _imageView.frame;

    // Get scroll view bounds
    CGRect  bounds;
    bounds = _scrollView.bounds;

    // Compare image size and bounds
    rect.origin = CGPointZero;
    if (CGRectGetWidth(rect) < CGRectGetWidth(bounds)) {
        rect.origin.x = floor((CGRectGetWidth(bounds) - CGRectGetWidth(rect)) * 0.5f);
    }
    if (CGRectGetHeight(rect) < CGRectGetHeight(bounds)) {
        rect.origin.y = floor((CGRectGetHeight(bounds) - CGRectGetHeight(rect)) * 0.5f);
    }

    // Set image view frame
    _imageView.frame = rect;
}

あとは、UIScrollViewDelegateメソッドを実装する。viewForZoomingInScrollView:でUIImageViewを返す。そして、スクロールビューがズームされるたびに呼び出されるscrollViewDidZoom:メソッドで、_updateImageViewOriginを呼び出す。これにより、常にUIImageViewが中央に表示される事が担保される。この呼び出しが、この手法の最大のポイントだね。

- (UIView*)viewForZoomingInScrollView:(UIScrollView*)scrollView
{
    return _imageView;
}

- (void)scrollViewDidZoom:(UIScrollView*)scrollView
{
    // Update image view origin
    [self _updateImageViewOrigin];
}

これでできるよん。

ここまでのソースコード:ScrollTest.zip

ビューの回転難しい


あー、もう、ビューの回転って難しいな!UIViewControllerを使ったビューの回転のプログラミングを書いているんだけど、これが何回やっても上手にできなくて。

単純に回転するだけならいい。最も単純な回転とは、回転するとそれにあわせて中のビューの大きさが外部のビューの大きさとピッタリ同じに変わってしまう場合。

これが、回転しても比率を保とうとすると、めんどくさくなってくる。縦と横とでスケールを変えなくてはいけない。さらに、縦と横とで表示する画面を変えると、さらにめんどくさくなってくる。たとえば、縦だと1ページ、横だと2ページの見開きにする、って場合ね。

これに、スクロールビューをからめて拡大縮小も可能にすると、もう何が何だか。zoomScaleを考慮しないといけないし、更新のタイミングも回転時だけではなくてズーム時になるし。

いっつも試行錯誤してしまう。充分に検討して、パターン化できるといいんだけど。

いまさらながらのLion


開発用Macがもう1台欲しかったんで、iMacを買った。27インチのやつ。iPadアプリ開発が始まってからは、シミュレータを動かすため、27インチは必須だね。

届いたからセットアップしようと起動したら、うん?なんか見たことない画面が。うぉぉぉ、これがLionか!そういや、触るの初めてだ。現行で使っているマシンは、アップデートの時間やトラブルが発生したらめんどくさいので、Snow Leopardのまま放っておいてた。初めてのLionマシンだ。

ほんのちょっとだけ触って思ったのが、ミッションコントロールとフルスクリーンを基調とするこのユーザインタフェースは、タイリングウインドウの復権だ。GUIのウインドウシステムってのは、極初期に画面を分割するタイリングウインドウがあって、その後すぐにウインドウが重なるオーバーラップウインドウが主流になった。Macはもちろん産まれたときからオーバーラップで、タイリングウインドウっていうとどこのWindowsだよ、って感じだ。

でもiOSの隆盛があって、シングルウインドウ+タイリングウインドウが復活してきた感じ。ほんとIT業界ってのは、10年周期くらいで同じ技術や発想使い回すよね。10年前と違うのは、画面がものすごくでかくなったこと。これだけでかいと、かえってオーバーラップが使いにくい。

で、開発者として思いおこすのは、Xcode 4のこと。Xcode 4は一年前くらいに登場したけど、いままでとはドラスティックにインタフェースを変えてきた。一言で言うと、シングルウインドウ+タイリングウインドウの構成にしてきたんだよね。今にして思えば、これはLionのユーザインタフェースを予言していたわけだ。

Xcodeは開発環境だけど、何気にユーザインタフェースが野心的で実験的だ。ここからAppleの未来を垣間みることが出来る。

iOS 5 GMを待ちながら


毎日iOS Dev Centerをチェックしては、iOS 5 GMが出ねぇ、とつぶやいている日々であります。忙しい日々なんだけど、GMおよび正式リリースの日程が明らかにならないので、微妙にまったりとした感じが。

果たしていつになるのか?という予想はするだけ無駄なんで、Macお宝鑑定団blogの記事を読んで気を紛らわせたり。まー、正直なところ、人の会社の行動を予測して気を揉んでもつまらないので、自分の会社の心配だけをしておけ、と考えるようにしている。

GMが出たら、最終的な動作チェックを行うことになる。このチェックで問題は出るのか?この期に及んでAPIを変えてくるということは考えにくいので、そこは問題ないと思う。気になっているのは、アーキテクチャ的なことだな。

たとえば、ARMv6のサポート。iOS 5 SDK betaのコンパイラは、ARMv6用にコンパイルすることができていない。これが正式版になっても、切り捨てられたままなのか?もちろんiOS 5はARMv6では動かないけど、昔のデバイスを対象にしながら、iOS 5 SDKでビルドしたい、という必要はある。

あと、コンパイラ。iOS 5 SDK betaのたぶん7から、デフォルトコンパイラがApple LLVMになってた。いままではGCCのLLVMだったのに。正式版でもApple LLVMがデフォルトになるのか?自分で書いたソースコードだけなら対応できると思うけど、外部のライブラリがある場合は嫌だな。コンパイラ切り替えればいいんだけど、デフォルトが変わっちゃうのはやっぱり気になる。

なんてことを気にしつつ、日々のニュースチェックです。iPhone 5なんかよりも、 iOS 5 GMの方が気になるよ、ホントに。

idleTimerDisabledではまったこと


UIApplicationクラスに、idleTimerDisabledっていうプロパティがある。これをYESにしておくと、システムで設定した、スクリーンロックを無効にする事ができる。地図とかゲームみたいに、しばらく触らないで放っておくタイプのアプリでは、便利。

と、ここまではドキュメントを読めば分かる事。これを使ったアプリはいろいろ作っていたんだけど、これが機能しないというバグ報告があった。ふーんどれどれ、ということでソースコードをチェックして、あーなんかここ怪しいなー、というところを修正してテスト。まずはidleTimerDisabledをYESにして放置。2分後に確認すると、スクリーンロックしていない。よし、OK。次にidleTimerDisabledをNOにして放置。2分後に確認すると、、、ロックしていない。あれー?

昔からこれ使っているのになんで動作しないんだ?ということで、ソースコードの変更を繰り返しながら延々とテストを繰り返す。うまく動いてくれない。正確に言うと、ロックしてくれない。なんか、ずーっとロックしてくれないような。。。というところで気づいた。USBでつないでXcodeからアプリを起動した場合、スクリーンはロックしてくれないんだ。ケーブル外してテストしたところ、ちゃんと期待通りに動いてくれた。

何が嫌だったって、動作確認するのに毎回2分かかったんだよね。ちょっとコード変更して2分放置。また変更して2分放置。時間が大変浪費されたのでした。

Xcode 4のポッチ


Xcode 4に移行したときからずっと気になっていたものがあって、エディタウインドウの左端に、丸いポッチが表示されていることがあるでしょ。

なんか、BCGの跡みたいで嫌だなー、キモチワリー、とだけ思っておりました。

よく見ていると、どうやらIBOutletとIBActionについているらしい。ふーん、とだけ思って、何のためにあるのか全然考えた事がありませんでした。

で、ある日何の気なしにクリックしてみると、なんと接続している.xib上の部品を表示するじゃないすか。選択すると、そのファイルにジャンプしてくれる。

おー、これは便利。と思っていたら、何気なくポッチからドラッグしてみると、線が引っ張りだせるじゃないすか。もしかして、と思ったらそのまま直接.xibの部品と接続できるじゃない。

うぉー、すげー!アシスタントウインドウ開いているときに便利だ。と思っていたら、もしかして、と気づいた。今度は.xibファイルの方から.hファイルに線を引っ張ってみると、なんとアウトレットとアクションを追加するための特別なユーザインタフェースが出てくるじゃないすか!

なんてこったい!全然気づかなかったよ。これがInterface BuilderをXcodeに統合した利点か。

今回の教訓。Xcodeにはまだまだ知らない機能がたくさんあるようだ、ということ。シンプルなユーザインタフェースは、初見ではどうやって使うか分からないこと。でも一旦分かってしまえば、応用が効くようなユーザインタフェースにするのが重要だという事。

で、最後なんだけど、この「ポッチ」って名前はなんていうだろう?名前が分からんから、Googleで検索もできんし、マニュアルで調べる事もできん。

UIViewControllerによるUIViewの回転 その2


前回に続き、UIViewControllerの話。今回は、親ビューとの関係を。

UIViewControllerを使って回転させると、管理しているビューのboundsとtransformが変化する。となると次は、親ビューでの位置を表すframeが気になってくる。だけど、UIViewのドキュメントによると、transformが単位行列以外の場合、frameの値は不定(undefined)ということになっている。だから、centerの値から考察するのが正しい。

ということで、ビューの回転とboundsおよびcenterの値を調べてみた。ウインドウの上に直接UIViewControllerを貼付けて、4方向に回転させる。結果はこんな感じ。

うーん?分かったような分からんような。

ポイントその1。向きによってboundsの値は同じであっても、centerの値は異なる。縦の正位置と逆さま位置、および横の右向き左向きは、それぞれboundsの値は同じだけどcenterの値が異なる。

ポイントその2。縦と横でcenterのずれる値の座標が違う。縦だとy座標がずれて、横だとx座標がずれる。

なんでこんなことが起きているかというと、boundsは子ビューの座標系で測るのに対して、centerは親ビューの座標系での値になるからだ。つまり、boundsはtransformの影響を受けるが、centerは受けない。親ビューの座標系では、デバイスがどんだけ回転しようと、デバイス左上が原点だ。

さらに話をややこしくしているのが、ステータスバーの存在。デバイスを回転すると、ビューが回転するけど、ステータスバーも回転する。ということは、その分centerは移動しなくてはいけない。

ということで、それぞれの向きの位置情報について、もう少し突っ込んで記述すると、こんな感じになる。

これで、boundsとcenterの関係が分かったでしょ。デバイスの回転とともにステータスバーも回転するんで、ステータスバーの高さである20ピクセルのずれが発生している。左上原点のところにステータスバーが存在するかどうかがポイントだね。あと、この図で見るとまるでcenterが回転しているみたいだけど、こっちじゃないからな。回転しているのはあくまでboundsの方だぞ。

UIViewControllerによるUIViewの回転 その1


iPhoneが登場したとき話題をさらったのは、デバイスを回転させるとそれに追随して中の画面も回転すること。いまとなっては当たり前のような機能だけど、登場した当時は魔法のように見えたものだった。

あまりに衝撃的で、かつ直感的で自然に見えたため、まるで回転に対応するアプリを作るのはとても簡単だと思われることもある。でもねー、かなり面倒くさいんだよね、これ。単純な話として、縦画面と横画面を作らなくてはいけないので、手間が倍になる。それはしょうがないからがんばるとして、どうやって回転を検知して、どうやって回転に対応する描画を行うか、というのが思ったよりも複雑だ。

ということで、回転にまつわるアレコレをまとめてみた。先に結論を言うと、回転に対応するただ一つの正解は存在しないみたいなので、アプリの仕様に応じていろいろと使い分けが必要になると思う。道具立てを紹介することは出来る。でも、どれを使うかはあなた次第。

では、回転をサポートする、もっとも簡単な方法から紹介していこう。それはもちろん、UIViewControllerを使うこと。UIViewControllerには、shouldAutorotateToInterfaceOrientation:というメソッドがある。これを上書きしてYESを返すようにすれば、それで回転することになる。

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)orientation
{
    return YES;
}

実装の仕方によって、縦画面のサポートや横画面だけのサポート、またはあるタイミングでは回転を許して、あるタイミングでは許さない、ということも実現できる。

回転が発生すると、willRotateToInterfaceOrientation:duration:、willAnimateRotationToInterfaceOrientation:duration:、didRotateFromInterfaceOrientation:といったメソッドが順次呼び出される。回転時に特別なアニメーションを行いたいときは、これらの中で対応する。

というのが、簡単な使い方。

さて。もう少し突っ込んでみよう。そもそも、「回転」とは何だ?「回転が発生する」とは、いったい何が起きたことを意味するのだ?

答えは、UIViewの座標系が変化した、と言うことが出来る。UIViewには、大きさを表すbounds、アフィン変換を表すtransformといったプロパティがある。これらが変化することで、回転が実現されるのだ。UIViewControllerの場合、管理しているviewプロパティに対して、これらの属性をアニメーションとともに変化させることになる。

実験してみよう。UIViewContorllerにある回転イベントを通知するメソッドで、ビューのframe、bounds、transformといった値を表示させてみる。こんな感じのソースコード。

- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)orientation
        duration:(NSTimeInterval)duration
{
NSLog(@"### willRotateToInterfaceOrientation:duration ###");
NSLog(@"frame %@", NSStringFromCGRect(self.view.frame));
NSLog(@"bounds %@", NSStringFromCGRect(self.view.bounds));
NSLog(@"transform %@", NSStringFromCGAffineTransform(self.view.transform));
}

- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)orientation
        duration:(NSTimeInterval)duration
{
NSLog(@"### willAnimateRotationToInterfaceOrientation:duration: ###");
NSLog(@"frame %@", NSStringFromCGRect(self.view.frame));
NSLog(@"bounds %@", NSStringFromCGRect(self.view.bounds));
NSLog(@"transform %@", NSStringFromCGAffineTransform(self.view.transform));
}

- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)orientation
{
NSLog(@"### didRotateFromInterfaceOrientation: ###");
NSLog(@"frame %@", NSStringFromCGRect(self.view.frame));
NSLog(@"bounds %@", NSStringFromCGRect(self.view.bounds));
NSLog(@"transform %@", NSStringFromCGAffineTransform(self.view.transform));
}

結果はこんな感じ。

(縦から右横に回転)
### willRotateToInterfaceOrientation:duration ###
frame {{0, 20}, {320, 460}}
ounds {{0, 0}, {320, 460}}
transform [1, 0, 0, 1, 0, 0]
### willAnimateRotationToInterfaceOrientation:duration: ###
frame {{20, 0}, {300, 480}}
bounds {{0, 0}, {480, 300}}
transform [0, -1, 1, 0, 0, 0]
### didRotateFromInterfaceOrientation: ###
frame {{20, 0}, {300, 480}}
bounds {{0, 0}, {480, 300}}
transform [0, -1, 1, 0, 0, 0]

(右横から逆さまの縦に回転)
### willRotateToInterfaceOrientation:duration ###
frame {{20, 0}, {300, 480}}
bounds {{0, 0}, {480, 300}}
transform [0, -1, 1, 0, 0, 0]
### willAnimateRotationToInterfaceOrientation:duration: ###
frame {{0, 0}, {320, 460}}
bounds {{0, 0}, {320, 460}}
transform [-1, 0, -0, -1, 0, 0]
### didRotateFromInterfaceOrientation: ###
frame {{0, 0}, {320, 460}}
bounds {{0, 0}, {320, 460}}
transform [-1, 0, -0, -1, 0, 0]

まずは、transformに注目。最初は[1 0][0 1]。つまり、単位行列(identity)だ。次に[0 -1][1 0]。そして、[-1 0][0 -1]と、変化してく。つまり、90度ずつ回転していっているわけだね。この辺の数学的な話は省略。

続いてboundsを見てみる。最初は{320, 460}。次に{480, 300}。そして{320, 460}。これも、回転した画面の大きさにあわせて変化していっていることが分かる。

つまり、ビューの回転とは、boundsとtransformが変化した、ってことだ。

frameと、親ビューとの関係については、次回。