技術をかじる猫

適当に気になった技術や言語、思ったこと考えた事など。

iPhone で今更 HelloWorld(InterfaceBuilder使わずに実装)

Xcode についてくる InterfaceBuilder についての感想の遷移。

①初めて触ったとき → 超意味不。本気で使いにくい。
②すべてプログラムで実装した後 → InterfaceBuilder ってそういうことやってたのね。だがコードで書く。
③今 → とりあえず InterfaceBuilder で見た目だけ作って、動いてからプログラムに置き換える。

順を追って振り返る。
始めに、様々な入門書で InterfaceBuilder を使ったコーディング入門をしている。これは、本に限ったことではなく、Web でもそう。
全く何も知らずに「はいできたー」と、のたまうならそれでも良いかもしれない。しかし本職でそれは致命的だと思う。カスタマイズもできなければ、動的な画面生成もできないからだ。
そして、見事にそれにハマった。InterfaceBuilder で作ったものの実体、どうやって動くのか、それが入門で説明しないからだ。
ごもっとも。そんなの知る必要があるのはこだわりを持つか本職かだけだ。
そこで最初にとった行動は、InterfaceBuilder を使わずにコーディングしてみるということだ。

最初に誰もが通る HelloWorld を僕はこう作った。

  • テンプレートを全部試してみる。
  • 共通部分を比較してみる。
  • 最も基本となるテンプレートからプロジェクトを作成する。

上記の手順で行き着いたのが Window-based Application だった。
main があって、AppDelegate を呼び出して、window と呼ばれる画面が存在するこの構成だ。

構成的には、こんな関係っぽい*1

main -> AppDelegate  <-|  |-  window
             ^         |壁|     ^
      ViewController <-|  |-  UIView

UIViewController には、サブコントローラほ保持できるし、それに合わせて、UIView も下階層で保持できる。
個人的にはあまり深くはしたくないかな、、、、
Window-based Application では、main,AppDelegate,window のセットしかない。

main -> AppDelegate  <-|  |-  window

なので、ViewController は手前味噌で実装した(今はまだしも 09/06 位は殆ど英語しかなかった)。
ヘッダ。

#import <UIKit/UIKit.h>

@interface HelloViewController : UIViewController {
    UILabel* labelHello;
    UIButton* buttonPush;
}

- (IBAction)pushHello;

@end

本体(参照カウントがわかりやすいように手動で数えてます)。

#import "HelloViewController.h"

@implementation HelloViewController

- (id)init {
    if (self = [super init]) {
        // MVC の M 層なんかはこの辺で初期化しとく。
        // 今回はなし。
    }
    return self;
}

// nib(XIB) 抜きでインスタンス作るときのイベント。
// MFC の OnCreate とかそのへん。
- (void)loadView {
    
    // alloc で参照カウント 1 [self addView:]で参照カウント 2
    self.view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 320, 500)];
    
    // 参照カウントを 1 にしておく。
    [self.view release];
    
    // alloc 時点で参照カウント 1
    labelHello = [[UILabel alloc] init];
    [labelHello setFrame:CGRectMake(20, 100, 300, 21)];
    [labelHello setText:@"Push button."];
    
    // addSubview で参照カウント 2
    [self.view addSubview:labelHello];
    
    // buttonWithType でボタンを作ると、autorelease なので、
    // このメソッドを抜けると参照カウントが 1 減る。
    // 変数に保持させたいので、retain で参照カウントを増やしておく(参照 2)。
    buttonPush = [[UIButton buttonWithType:UIButtonTypeRoundedRect] retain];
    [buttonPush setFrame:CGRectMake(100, 140, 80, 40)];
    [buttonPush setTitle:@"hello" forState:UIControlStateNormal];
    
    // イベントを設定しておく
    // ボタンを押されたら、自インスタンスの pushHello を起動。
    [buttonPush addTarget:self
                   action:@selector(pushHello)
         forControlEvents:UIControlEventTouchUpInside];
    
    // addSubview(参照 3)
    [self.view addSubview:buttonPush];
    
    // メソッド終了(buttonPush の参照が 2 になる)
}

// ボタン押されたときのイベント。
- (IBAction)pushHello {
    [labelHello setText:@"Hello world!"];
}

- (void)viewDidUnload {
}

- (void)dealloc {
    // 両方参照カウント 1 へ
    [buttonPush release];
    [labelHello release];

    // self.view は消えて、子に持ってる連中の参照カウントが全部 1 減る。
    [super dealloc];
}

@end

で、これを AppDelegate に読ませれば起動できる。

@interface HelloWorldAppDelegate : NSObject <UIApplicationDelegate> {
    UIWindow *window;
    UIViewController* controller; // 追加
}

@property (nonatomic, retain) IBOutlet UIWindow *window;

@end
- (void)applicationDidFinishLaunching:(UIApplication *)application {    

    // Override point for customization after application launch
    controller = [[HelloViewController alloc] init];
    [window addSubview:controller.view];
	
    [window makeKeyAndVisible];
}

- (void)dealloc {
    [controller.view removeFromSuperview];
    [controller release];
    [window release];
    [super dealloc];
}

で、InterfaceBuilder 使うコードと比較して何が違うかというと、

  • UIViewController 初期化時に、initWithNibName を使用しない(nib/xib ファイルを使わないから)。
  • loadView で画面を生成してる(InterfaceBuilder が本来サポートしてる部分)
  • dealloc で参照カウント制御してる(autorelease しとけば無視もできる)

要するに loadView の所でコード書かなくて済むだけ。
よほど複雑な画面作るのでなければ、コードで書いた方が実行速度早いしおすすめ〜。
とはいえ、見た目に時間かけるより前に、ロジックを確認したい場合も確かにある。なので、最近はこんな使い方をする。

  • InterfaceBuilder で画面を適当にでっちあげる
  • 内部処理がうまい事動いてるのだけ確認する
  • loadView メソッドを作成して、initWithNibName を init に移植する。

もっといい使い方があって、スパゲティにならない方法があれば是非。

*1:実際にはこれも把握するのに2−3日苦しんだのだが

iPhone でアラート出す。

といってもこれだけ。

- (void)alertView:(UIAlertView*)alertView
clickedButtonAtIndex:(NSInteger)buttonIndex {

    /* 何かの処理 */

    [alertView release];
}

- (IBAction) selectTimeTrial {
    UIAlertView* alert = [[UIAlertView alloc]
        initWithTitle:@"タイトル"
        message:@"このコード書くのに何分?"
        delegate:self cancelButtonTitle:nil
        otherButtonTitles:@"1 分", @"3 分", @"5 分", nil];
    [alert show];
}

iPhone でちまちまゲーム作ってみる

作るゲームの内容とか、全コード曝すことはしない。
あくまで Tips というか、作る間にハマったこと、ノウハウとか書くだけ。

ゲームは複数の画面があって、当然のごとく切り替える。タイトル、ゲーム画面、ヘルプ、設定画面位はよくある構成か、、、。

もちろん、画像や音声の事も考えるなら、画面を切り替えたらリソースは常に解放するのが正しい。特に音声リソースはかなり容量を食うので、画面遷移の前にリソースを解放する。
では、どうやってリソースを解放するのかという話になる。その前に、ゲームの構造を考えてみる。
まず、どこで画面を切り替えるのか考える。
画面を階層化して管理できる手前、管理用の UIViewController を置く事も考えられるが、UIViewController のネストは、Cocoa の画面管理で、Quartz で言えば、とにかく画面があればよい程度。
リソースも抑えたいので、画面切り替えは AppDelegate にやらせてしまうのがよさげ。

で、このときに画面切り替え用メソッドを、 AppDelegate に突っ込んどく。

enum SceneID {
  SceneIDTitle,
  SceneIDGame,
  SceneIDHelp,
  SceneIDScore,
};

/* 中略 */

- (void)changeScene:(int)sceneId;

で、各 ViewController からは、AppDelegate に対して、画面切り替えせーというメッセージを送る。
このとき、画面4つ全部、AppDelegate への参照を個別に持たすのは面倒なので、親クラスを用意しておく。

@interface ControllerBase : UIViewController {
	XXXXAppDelegate* gameAppDelegate;
}

@property(nonatomic,assign) id gameAppDelegate;
@end

@で消すときとか面倒だし、assign でいいと思った。で、これを各ゲームシーンのコントローラで継承する。継承万歳。

次に画面切り替えのメソッドを実装してみる。

ヘッダはこんな感じで、

@class ControllerBase;

@interface XXXXXXAppDelegate : NSObject <UIApplicationDelegate> {
    UIWindow *window;
    ControllerBase* controller;
    ControllerBase* oldController;
}

@property (nonatomic, retain) IBOutlet UIWindow *window;

- (void)changeScene:(int)sceneId;

@end

実装はこんな感じか?

- (ControllerBase*)sceneFactory:(int)sceneId {
	
	ControllerBase* selected = nil;
	
	switch (sceneId) {
		case SceneIDHelp:
			selected = [[HelpController alloc] initWithNibName:@"HelpController" bundle:nil];
			break;
		case SceneIDTitle:
			selected = [[TitleController alloc] initWithNibName:@"TitleController" bundle:nil];
			break;
		case SceneIDGame:
			selected = [[GameController alloc] init];
			break;
		case SceneIDScore:
			selected = [[ScoreRecordController alloc] initWithNibName:@"ScoreRecordController" bundle:nil];
			break;
		default:
			break;
	}
	
	[selected setGameAppDelegate:self];
	
	return selected;
}

- (void)animationEnd {
	[oldController release];
	oldController = nil;
}

- (void)changeScene:(int)sceneId {
	oldController = controller;
	controller = [self sceneFactory:sceneId];
	
	[UIView beginAnimations:nil context:NULL];
	[UIView setAnimationDuration:0.5];
	[UIView setAnimationDidStopSelector:@selector(animationEnd)];
	[UIView setAnimationTransition:UIViewAnimationTransitionCurlUp forView:window cache:YES];
	
	[window addSubview:controller.view];
	[oldController.view removeFromSuperview];
	
	[UIView commitAnimations];
	
	[oldController release];
}
  • ゲームシーンの変更要求が来たら、sceneFactory でインスタンスを作る。Factoryパターン万歳
  • UIView animation で画面切り替える。
  • animationEnd でアニメーションの終了を補足して、リソース解放。

で、ざっくりメモリ解放する。
dealloc とか実装忘れちゃやーよ。

OS3.0 の公式 UnitTest 試してみた

さくっとテスト対象プログラム

@interface HelloObject : NSObject {
}
- (NSString*)helloMessage:(NSString*)name;
@end
#import "HelloObject.h"
@implementation HelloObject

- (NSString*)helloMessage:(NSString*)name {
	return [NSString stringWithFormat:@"Hello %@.",name];
}

@end

この後、ターゲット追加で、UnitTest を追加。

名前は LogicTests でいーや
こいつをアクティブターゲットに設定する。
Tests グループ作ってテストコード隔離する。
Objective-C test case class を選択して作成。これも HelloTests でいーや。

ここで HelloTest をターゲットに指定する。
とゆーか普通のプロジェクトにこんなもん組み込んでもしょうがねえ。

今回はアプリケーションテストしないのでさくっとテスト書く。

@interface HelloTests : SenTestCase {
}
- (void)testHello;
@end

テストの本体

#import "HelloTests.h"
#import "HelloObject.h"

@implementation HelloTests

- (void) setUp {
	// Set up
}

- (void) tearDown {
	// Tear down
}

- (void)testHello {
	HelloObject* hello = [HelloObject alloc];
	[hello autorelease];
	
	STAssertTrue(
		[[hello helloMessage:@"Azalea"] isEqualToString:@"Hello Azalea."],
		@"Hello test is failed."
	);
}

@end

これだけではまだテストできない。
まぁテスト対象のコードをバンドルに突っ込むだけだけど。

とりあえずこれでロジックテストは実行できる。
ただし「Simulator」の「OS 3.0 以上」でのみ。

BAD_ACCESS のポイントを探る

Xcodeメニューで[メニュー][プロジェクト][アクティブな実行ファイルを編集]を選択。
環境変数で以下の3つを指定

NSZombieEnabled = YES
MallocStackLogging = YES
NSDebugEnabled = YES

上記で、BAD_ACCESS の原因クラスが判明。

shell malloc_history [PID] [0x...のアドレス]

突っ込むと、alloc した位置をトレースできる。

実際には ZombeEnabled だけでデバグできるらしい。
http://www.codza.com/how-to-debug-exc_bad_access-on-iphone