IBを使わずに作るiPhoneアプリ作成入門:第2回
こんにちは、亀本です。目下の悩みはブログのタイトル欄の短さです。サブタイトルつけると長すぎてダメぽ!
そんなことはさておき、前回はそこそこ反響をいただいたようで、ちょいちょいとiPhoneアプリ開発の連載を続けていこうかと思います。
最初は続きを書くか迷っていて、忙しさもあってちょっと間が空いてしまいましたが、これからはできるだけちょこちょこと書いていけるようにしていきます。
第1回はこちら:http://blog.asial.co.jp/502
第2回に今さら言うのもアレですが、全体の流れとしては、まずはいろんなView / Controllerの使い方を説明し、個別の機能を理解するとともに、iPhoneアプリケーションで採用されているMVCフレームワークの構造を紹介していきます。
それが終わった後で、何かそこそこ楽しめそうなアプリ作成を順を追って説明できればいいかな、なんて思っています。
時にはIB(Interface Builder)との比較も用いながら、IBのどの役割をどのメソッドが担当しているのか、などを解説できたら、IB無しでも構造的にきれいなMVCプログラミングができるんじゃないか思って、その辺を目標にしています。
# ただし、そのあたりの説明は自分の経験則になってくるので、正確性は保障できないのですが。。。間違いがあったらぜひ突っ込みをお願いします><
今回の目標
今回は、前回適当に貼り付けたテーブルをカスタマイズする方法を通じて、Controllerの基本的な使い方を説明していきたいと思います。
まだiPhoneのMVCフレームワークがどんな構造か、までは説明が到達しません。
デキるプログラマの人には冗長な解説かもしれませんが、ゆっくりゆっくり。
テーブルをカスタマイズする方針としては、前回使用したUITableViewControllerを継承したクラスを作成し、テーブルの性質や動作を定義するための各メソッドをオーバーライドしていきます。
iPhoneでの開発は、基本的にはこの方針のようにフレームワークの用意したControllerをオーバーライドする形で進みます。
新しいコントローラの生成
まずは新しいコントローラクラスを記述するファイルを生成します。
プロジェクトへのファイルの追加は、Xcode左側の「グループとファイル」のツリーで行うのが楽です。(メニューからもできますが。)
各種クラス実装は、基本的にsamplePrjプロジェクトのClassesグループ内に追加していきます。
ここでは、ClassesグループをCtrl+クリックして「追加」→「新規ファイル」と選択します。
すると、新規ファイルのテンプレートを選択する画面が開きます。
最初にプロジェクトを作成するときと同様、Xcodeでは各クラスを継承した新たなクラスを作る際のテンプレートも用意してくれています。
用意されているテンプレートは5種類ですが、言ってみればこの5種類がiPhone開発時のほとんどを占めることになる、ということです。
(なお、どれもただの「テンプレート」生成なので、後々自分が作りたいのに適切なものが無い場合には、適当なテンプレートを生成後、適宜書き換えましょう。)
ここでは、「UITableViewController subclass」を選択し、SimpleTableViewController.mという名前を付けて生成しましょう。
すると、Classesグループ内に新たにSimpleTableViewController.hとSimpleTableViewController.mという名前のついた2つのファイルが生成されます。
Objective-Cでは、クラスの定義はインターフェース定義(@interface)と実装定義(@implementation)の2つを別々に記述します。
主に.hファイル(ヘッダファイル)でクラスのインターフェースを定義し、.mファイルでその実装を記述するのが流儀です。
ここで、SimpleTableViewController.hを見てください。おそらく、次のような内容のファイルができていると思います。
この中身をざっと説明しましょう。
@interface SimpleTableViewController : UITableViewController {
}
@end
上のコードはただ単にUITableViewControllerクラスを継承し、SimpleTableViewControllerというサブクラスを定義しています。ただそれだけで他に何もしていません。
インターフェースの定義式としては
@interface SubClass : SuperClass <interface1,interface2, ...> {
// ここでメンバ変数定義
}
// ここでシグネチャ定義
@end
という形になります。(メンバ変数とシグネチャはおいおい説明します。)
クラスの定義は、このように@interface ~ @endまでの間に色々な物を記述する形 で行います。
クラス定義時には、:「コロン」で区切って親クラスを記述し、<>で囲った中に組み込むインターフェースを列挙します。必要ない場合には、それぞれ省略できます。
さて、新しいコントローラが生成できたところで、現在使っているUITableViewControllerから、SimpleTableViewControllerを使うように切り替えましょう。
sampleProjAppDelegate.mを、以下のように修正します。
#import "samplePrjAppDelegate.h"
#import "SimpleTableViewController.h"
@implementation samplePrjAppDelegate
@synthesize window;
- (void)applicationDidFinishLaunching:(UIApplication *)application {
SimpleTableViewController *simpleTableViewController = [[SimpleTableViewController alloc] initWithStyle:UITableViewStylePlain];
[window addSubview:simpleTableViewController.view];
// Override point for customization after application launch
[window makeKeyAndVisible];
}
.
.
.
まず2行目で、SimpleTableViewController.hをimportで読み込んでいます。
importとは、PHPで言うところのrequire_onceに当たるもの(仕組み自体は違いますが)で、まだ読みこまれていなければそのファイルを読み込む、というものです。
ユーザ定義のクラスを利用するには、このように#importを使ってヘッダファイルを読み込む必要があります。
実装コード側では、UITableViewControllerを指定していた部分が、SimpleTableViewControllerに置き換えてあります。
これでビルドを実行すると、SimpleTableViewControllerが利用されるようになります。
見た目的には変わりませんが。。(^^;
セル内容の表示
それでは、新しいテーブル内にデータを流し込んでみましょう。
まず、SimpleTableViewControllerにデータの格納用メンバ変数を定義します。
メンバ変数の定義はインターフェース部で行います。SimpleTableViewController.hを次のように編集します。
@interface SimpleTableViewController : UITableViewController {
NSArray *myDataSource;
}
@property (nonatomic, retain) NSArray *myDataSource;
@end
myDataSourceというメンバ変数を定義しています。この変数に、テーブルに表示するデータを定義します。
まず、@interfaceのクラス定義の{}ブロック内に記述してあるのが、メンバ変数の定義です。定義は、型の指定と変数名の指定で行われます。
ここでは、NSArrayオブジェクト型(のポインタ)を格納するmyDataSorceという変数を定義しています。
(※なお、NSArray型とは、Objective-Cのフレームワークに組み込みの、配列オブジェクトです。)
また、ブロック外に新たに@propertyという指定が追加されていますが、これは指定したメンバ変数のsetterとgetterのシグネチャを同時に定義する糖衣構文です。
hogeというメンバ変数に対して、setHogeというsetterとhoge:というgetterのシグネチャが定義されます。
ここでは、myDataSourceが指定されているので、setMyDataSourceとdataSourceというメソッドのシグネチャが定義されたことになります。
(※あくまでも、シグネチャの定義だけであって、実装は別に.mファイルの実装部に記述しなければならない点に注意してください。)
@propertyの()内の指定は、自動生成されるsetter/getterの性質を定義するものです。ここではおまじない程度に思っておいてください(名前からなんとなく想像できるかもしれませんが)。
オブジェクト型を指定したメンバ変数定義の場合は、ほとんどの場合このおまじないの通りで大丈夫です。
次にSimpleTableViewController.mに実装を行い、myDataSourceの内容を読み込んでテーブルにデータを表示するようにしましょう。
修正すべき点は、以下の3つです。
1. synthesizeの追加
まず、@implementationの下に@synthesizeという指定を追加します。
(なお、@implementationは実装部の開始を示すもので、@interfaceと同様@implementation ~ @endの間に実装コードを記述します。)
@implementation samplePrjAppDelegate
@synthesize myDataSource;
@synthesizeは、指定したメンバ変数に対して、自動的に単純なgetterとsetterの実装を定義してくれます。
言ってみれば@propertyの実装版で、多くの場合対で使用されます。
この@synthesizeと@propertyを使うと、面倒なgetter/setterの定義が非常に楽に済みます。
2. テーブル内行数のカウント
テーブル内の行数設定は、tableView: numberOfRowsInSection: というメソッドで行われます。
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [myDataSource count];
}
メソッドの定義について、ざっくりとイメージだけつかめるような説明を書いておくと
という感じです。
修飾子はクラスメソッドなら+、インスタンスメソッドなら-になります。
引数セパレータは、関数名の一部だと思っても構わないようなレベルのもので、メソッド呼び出し時に第2引数を指定する際には、第1引数に続けてこのセパレータもきちんと記述しなければなりません。
文章としてメソッド名を記述する場合は、このセパレータも含めて記述したりもします。
上記の tableView:numberOfRowsInSection:というメソッドは、UITableViewControllerで定義されているメソッドで、戻り値が1つのテーブルで扱われるセルの数になります。
myDataSource変数はNSArrayオブジェクト型なので、NSArrayオブジェクトがもともと持っているcountメソッドを呼び出すことで、その配列要素数を取得できます。
これによって、myDataSourceに格納したデータの数だけテーブルのセルが用意されるようになります。
3. 各セルへのデータの追加
各セルの中身を実際に作成・処理するのは、tableView:cellForRowAtIndexPath:メソッドです。
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = @"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:CellIdentifier] autorelease];
}
// Set up the cell...
cell.text = [myDataSource objectAtIndex:indexPath.row];
return cell;
}
このメソッドは、生成されたTableViewのセル数分だけループ実行され、第2引数のindexPathに現在のループのセル番号(0スタート)が渡されます。
上記のコード中で、自動生成されたコードから変更されているのは下から3行目の
cell.text = [myDataSource objectAtIndex:indexPath.row];
という行だけです。Objective-Cのメソッドは割と英文として読みやすいものが多いので、何となく動作の想像がつくのではないかと思います。
objectAtIndex:もNSArrayの組み込みメソッドで、指定したindexの配列要素を返します。また、indexPath.rowは現在のセル番号です。
ここでの処理は、myDataSource配列の中から指定したオブジェクトを取り出して、cellのtext要素の中に渡す、というものです。
その前の部分の処理は、細かくは説明しませんが、
① セルのオブジェクトに付与する固有名としてCellIdentifierという静的な変数を作成
② CellIdentifierをキーにして、既にCell オブジェクトを生成済みならそれを使いまわす。
③ もし生成されていなかったら、新たにCellIdentifierをキーとしてCellオブジェクトを生成
という流れです。
これをやっている理由は、Cellオブジェクトの生成コストの問題があるからです。
データが1つや2つの生成なら問題ないのですが、テーブル内のセル数がどんどん増えて行った場合、毎回生成していたのではオブジェクトの生成だけでかなりのパワーを要しますし、メモリの消費も多くなります。
iPhoneはそれほど強力なマシンではないので、こうして無駄なメモリ消費を抑えるような工夫をしています。
この自動生成コードだと、Cellという名前の1つのインスタンスをひたすら使いまわしています。
ともかく、これでセル内にテキストを渡す仕組みは実装できました。
データの流し込んでみる
それでは、sampleProjAppDelegate.mを以下のように編集してビルドしてみます。
- (void)applicationDidFinishLaunching:(UIApplication *)application {
NSArray *data = [[NSArray alloc] initWithObjects:
@"hoge",
@"foo",
@"bar",
nil
];
SimpleTableViewController *simpleTableViewController = [[SimpleTableViewController alloc] initWithStyle:UITableViewStylePlain];
[simpleTableViewController setMyDataSource: data];
[window addSubview:simpleTableViewController.view];
// Override point for customization after application launch
[window makeKeyAndVisible];
}
変更点は、dataというNSArray型の変数が追加されたのと、それをsimpleTableViewControllerのsetterに渡している点のみです。
なお、補足しておくと、NSArrayの生成のところで使っているinitWithObjects:は前回説明したイニシャライザです。このメソッドは、引数にカンマ区切りで続けて並べたオブジェクトをすべて配列の要素として格納していきます。
配列の終端を指定するには、nil(空のポインタ)を指定します。
なお、直感的にわかると思いますが@"hoge"などという指定は文字列オブジェクトであるNSStringの生成、初期化の糖衣構文です。
ここでは、hoge,foo,barという3つの文字列オブジェクトをdata配列に追加している、ということになります。
実際にビルドしてみると、以下の画像のような結果が表れると思います。
まとめ
これでひとまず、テーブルへのデータの流し込みの基本的なフローを見てきました。
今回やった事は
・新しいContorllerクラスの作成方法と定義、および使用方法
→@interfaceと@implementation
・メンバ変数とgetter、setter
→@propertyと@synthesize
・TableViewオブジェクトの操作のキソのキソ
やたらと長ーーーーーーーーい説明になってしまいましたが、判りにくいところとかおかしかったところはぜひぜひ突っ込みお願いします><
次回
次回は、ちょっとばかり理屈じみた話になります。
何でかというと、実は、1回2回と見てきたプログラムは、適切なメモリ管理の意識されていないプログラムになっています(※説明のため、そうなってます)。
そこで、次回はここから色々な知識を身につけていく前に、メモリの確保と開放をきちんと意識したプログラミングのための考え方と手法を説明していきます。
また、そろそろ「自分でメソッド調べてモリモリ書いてみるかな!」というノリの人が出始めるか なーなんて思うので、余裕があったらAPIリファレンスとかXcodeの補完機能の使い方等を説明しようかと思います。