演出やる人向けのObj-Cのコードの書き方、めも
これは
Objective-Cの講師をしていて演出系な人に説明した時の、こう書けば楽ができるよ、情報をまとめてみる。processingで演出をやっているけど、if文, switch文って知っているけど難しくって使えないな、という直感的なコーディングをしている方むけな感じで。環境はiOS4またはiOS5。iOS5のSDKをARC有効とします。コードは、意味が分かる程度で書くので、このままコンパイル通りません。
要は
- 目的:動かしてみてデバッグ、なんて必要を減らす。書いたら動く、そんなコードにする。
- 手段:コンパイラがエラーを出してくれるような書き方をする。
- 例:定数は定義する。動作パラメータやプロパティ名とか、スペルミスしたら動かないところは定数宣言にする。
- 手段:コンパイラがエラーを出してくれるような書き方をする。
- 目的:簡素で出戻りがいらないコードの作り方をする。
- 手段:何を作るかを紙に書いてから、画面に向かう。
- 例:モデル-View-制御、の3つの役割に分けて、考える。みたまんま(玉を動かすから玉のオブジェクトを作る、とか)だけでは、破綻する時がある(複数の玉が衝突する処理がいるとき、どこに衝突検出と反応のコードを書くか、わからなくなる)
- 手段:プログラムを作る考え方をする。
- 例:機能は何か、時間経過でどんな処理(機能)をするのか、モデル-View-制御はどう組めばいいか、を分析する。
- 例:”機能”は1つのメッセージにまとめる。メッセージの名前がその機能を、引数がどんな情報でその機能が動くのかを、明らかに示してくれる。
- 手段:何を作るかを紙に書いてから、画面に向かう。
位置などが変化する画像の表示
雪がうえからチラチラ降ってくるコードを見るとこんな感じだった。
@interface ViewController : UIViewController {
UIImageView *snow1, *snow2;
}
ViewControllerにUIImageViewを必要な数だけ変数宣言しておいて、
- (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@(timer:) userInfo:nil repeats:nil] } -(void)timer:(NSTimer *)timer { snow1.frame = ランダムな位置; 以下snow2に対して同じ処理... }
それの位置変更処理をNSTimerで一定時間ごとに呼び出して処理している。
変数は配列で扱うと便利
雪みたいな細かい画像を1つづつ処理してたら大変なので、配列にまとめます。配列毎に処理していきます。
//雪の数とかは、定義しておくといい。10個という直接の値(マジックナンバー)をコードのいろんな所で書き散らかすと、値を変更するときに手間、かつ、どこかで修正もれが出てバグる #define NUM_OF_SNOW 10 @interface ViewController : UIViewController { NSMutableArray *snows; //配列を宣言する }
UIImageViewを必要な数だけ変数宣言しておいて、
- (void)viewDidLoad { [super viewDidLoad]; snows = [NSMutableArray new]; for(int i =0; i < NUM_OF_SNOW; i++) { //10個の雪を表示することにする [snows addObject:[[UIImageView alloc] initWith...]; //雪のインスタンスを作る } [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@(timer:) userInfo:nil repeats:nil] } -(void)timer:(NSTimer *)timer { //for文+イテレータで、配列の要素を取り出す処理を自分で書かなくてもいいから楽。 //何番目の要素か分からないといけないときは、普通にfor(int i=0; i < [snows count]; i++) {...という書き方をする。 for(UIImageView *snow in snows) { snow.frame = ランダムな位置; } }
とすれば、雪の数の増減でコードをいじらなくてもいい。
一定時間経過した後に処理する
例えば、触られたらなにか反応して、その1秒後にまた反応するものを作るとして、タッチイベントで、sleep()とかすると、アプリの動きが止まる。
- (IBAction)someImagetouched:(id)sender { UIImageView *img = (UIImageView)sender; img.frame = 移動位置; sleep(1); //ちょっと経ってから img.frame = 次の移動位置; }
プログラムの実行は、スレッドという単位でコードが処理されている。UI周りはメインスレッドで処理されていて、一定時間ごとに処理すべき項目が順番に処理されていく(NSRunloop)。sleep()という関数は、スレッド自体の動きを指定時間止めるので、UIの処理の途中でsleep()したら全てのUI処理が停止する。こんな時は、非同期処理を使う。
- (IBAction)someImagetouched:(id)sender { UIImageView *img = (UIImageView)sender; img.frame = 移動位置; double delayInSeconds = 1.0; dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC); dispatch_after(popTime, dispatch_get_main_queue(), ^(void){ img.frame = 次の移動位置; }); }
この書き方で、dispatch_afterの処理は、一定時間が経過したら、NSRunloopが自動的に取り出して処理してくれる。
タイマーの使い方&UIViewクラス継承を使う
UIViewControllerにViewごとの処理を全て書いていくと、この処理はどのViewの処理だっけ?とごっちゃごっちゃになる。View1つで完結する処理は、Viewクラスにまとめてしまえば、コードの見た目がすっきりする。
例えば
@interface snowView : UIImageView { NSTimer *timer_; } -(void)start; -(void)stop; @end @implementation snowView -(void)start{ if(timer_ != nil) return; timer_ = [NSTimer scheduled...]; } -(void)stop{ if(timer_ == nil) return; [timer invalidate]; timer_ = nil; } -(void)timer:(NSTimer *) { 位置移動の処理; } @end
View毎にNSTimerを使ったらメモリ消費が、とか考えなくてもOK。たぶん、読み込んだ画像やUIViewのほうがよっぽどメモリを消費している。NSTimerをたくさん使うとNSRunloopの処理が増えてUIがカクカクになるのでは?っていうのは、タイマーの時間間隔を広めにして、アニメーションと組み合わせて、見た目を滑らかにする工夫をすればOK。
UIVewを継承したクラスを作っておくと、InterfaceBuilder(以下、IB)で自分が作ったクラスを配置できるから便利。IBでUIViewを適当に置いて、右ペインの左から3つめのタブをクリックして、Custom Classを例えば"snowView"にすれば、さっき作ったクラスインスタンスが配置できる。同じクラスをたくさん配置するときは、インスタンスが1つづつ配置されて面倒。こんなときはIBOutletCollectionで配列にまとめれば、扱いやすい。
外側に見せる変数はプロパティを使う
クラスの外側に見せるときは、”かならず”プロパティを使う。その理由は:
- どの値が外からアクセスできるのか、明示的にコードに表現できる。(ドキュメント並みの情報)
- 値変更のタイミングで処理を入れられる。
- 読み込みだけとか、参照がstrongかweakかなどのメモリ管理など、面倒なところを隠してくれる。(自動生成してくれる)
例えば、タイトル文字列を指定して、それが表示される処理はこんな感じ。
@interface snowView : UIImageView { public: //public変数でも、値が渡せそうだけど? NSString *title; } @property (strong, nonatomic) NSString *title; //明示的にセッターを定義してもいい。セッターは引数を取るから、メッセージの最後に必ず":"が必要なことに注意。 // @property (strong, nonatomic, setter=setTitle:) NSString *title; @end @implementation snowView @synthesize title; //プロパティの自動生成 // セッター -(void)setTitle:(NSString *)title { [setNeedDisplay]; } -(void)drawRect:(CGRect)rect { //実際の描画処理 } @end
まずプロパティでtitleを指定しておく。titleが変更されたら、それを表示に反映しないとけいないので、[setNeedDisplay]を呼ぶ。これを呼ぶと画面更新のタイミングでdrawRect:が呼ばれるので、その中で実際の描画処理をおこなう。UIViewを継承していると、UIViewの流儀にそった描画処理が使える。
プロパティの属性には、nonatomic/atomic, (NSObjectに対しては) strong/weak, read/readwrite、がある。
最初のは、変数アクセスの排他処理を行うか否か。いくつものスレッドが平行して走っているとき、1つのプロパティに同時に読み書きが生じる可能性がある。何も書かないときはデフォルトでatomic指定になる。atomicを指定してあれば、変数を読み書きできるのは1つのスレッドだけ、という排他処理を(コンパイル時に自動で)いれるので変なことにならない(設定するわけがない、化けた値が読み出されるとか)。だけど大抵のコードはUI処理とおなじメインスレッドで処理されていて排他処理は速度を低下させるだけの無駄なものになる。だからほとんどのコードでnonatomicがおまじないのように書かれる。
strong/weakは参照カウンタを+1するかしないかの違いで、デフォルトはstrong。ほとんどの場合strongでいいのだけど、delegateをプロパティで保持する場合は、メモリの参照循環が生じてメモリ解放されなくなるので、delegateはweak指定をする。これは、例えばUIViewControllerがUIViewを継承したインスタンスを持っていて、そのプロパティにselfをdelegateに入れたとする。UIViewControllerはすでにUIViewを参照して保持しているのに、UIViewがdelegateとして親のUIVivewControllerをstrongで保持してしまったら、互いに参照値を+1しているから、メモリが解放されない。delegateを指定するのは、そのインスタンスをすでに保持している場合だから、delegateまでstrongにする必要はない。だから、おまじないのようにweakにしちゃうのも、ありかもしれない。
readonly/readwriteは読み書きの設定。外部に伝えたいけど、変更されたら動作の支障になる値はreadonlyを指定しておけばいい。デフォルトはreadwrite。
あとはsetter/getterというのがある。プロパティは、見た目変数として使えるけれど、その実態はメッセージ。セッター/ゲッターの名前は自動生成ルール(set/getにプロパティ名の先頭を大文字にしたものが、デフォルトのメッセージ名)に従い、@synthesizeで指定したら自動生成されている。Key Value Observationの仕組みも、この自動生成されたコードに入ってくる。先の例のように、値の変更タイミング、取得タイミングで、いろいろと処理を行いたい場合がある。例えばUI表示に反映するするとか、値が妥当かチェックしてから取り込むとか。そんなときにゲッター/セッターの名前を指定するのがこの属性。
単純なpublicな変数として外部に値を解放するメリットは、ない。プロパティを使いましょう。
変数を置くのは、使う場所、共有すべき場所で分ける
クラスが使う情報をAppDelegateに置くと、AppDelegateに暗示的に依存するから、もしもあとでコードを取り出し再利用すると、AppDelegateに必要な実装がなくて動かないけれど、その原因がわからない、なんてことになる。だからAppDelegateに値を置くことは止めたほうがいい。どうしてもアプリケーション全体で共有すべきデータがあるなら、必要な情報を1つのクラスにプロパテイとしてまとめて(バリューオブジェクト)、それをinitのときに引数で渡せばいい。この仕組を入れておけば、例えば値が変更されたら、表示に反映するなどの動作を後からKVOで実装できる柔軟性が得られる。
目に見えない依存関係は、動かない原因を探すときに、とても厄介なものになる。データにしろ、機能にしろ、なにかに依存するなら、明示的に引数で渡す書き方をして、明らかに表現するのがいい。
delegateとKVOの使い分け
状態変化等の処理を外部に出したいときにdelegateを使います。@protocol で委譲したい処理がメッセージ名と引数で明確に表現できるのが利点です。値の変化をみるだけ、ならばKVOが便利です。プロパティのsynthesizeでKVOのコードが自動生成されるので、delegateを定義してコード内で通知するコードを書く手間がありません。通常delegateは1対1になります。処理を外部に委譲するのが目的なので、1対1になるだろう、ということです。例えばNSMutableArray *delegates;みたいにプロパティでreadonlyで外部に見せて、[delegates addObject:self], [delegates removeObject:self]とおまじないとすれば、その他のdelegateに影響せずに複数delegateも実装可能ですが、これが必要になる場合は、処理を外部に委譲する、というもともとの意味から逸脱していると思うので、構成などを見直すほうが楽でしょう。