Xcodeで新規プロジェクトを作成し、Mac OSXアプリケーションに仕上げます。
前回までで、Stopwatchクラスを設計してきました。
このクラスを利用して、Mac OSX上で動くアプリケーションを作ってみますよー。

こんな感じにします。よくばって、計測値のログを残せるようにしました。

計測中は、こんな感じ。
アプリケーションを作るとき、私はいつもこんなメモ書きをするようにしています。

まず画面に表示させたいインターフェースを想定して、
自前のクラス(AppController)を中心に、各インスタンスとの関係を考えます。
Stopwatchクラスが1つ、ログ格納用の配列が1つ、計測値更新用のタイマーが1つ。
AppControllerのインスタンス変数とメソッドの名前もここでだいたい決めてしまいます。
この段階がしっかりできると、あとの作業がはかどりますよー。
アプリケーションを設計するときは、どんなクラスをどのように利用するかを考えるわけですが、
データを表現するオブジェクトとGUI関係のオブジェクトを完全に分けるべしといわれています。
今回、データを表現するのは、Stopwatch、配列、タイマーが各1つ。
GUIに関与するのが、テキストフィールド、ボタン2つ、テーブルビューなど。
そしてこれらを連動させる仲介役として、AppControllerを設計します。
さてXcodeで、新規プロジェクト...から、Cocoa-Applicationを選んでください。

プロジェクト名は、LogWatchにしましょう。

そして早速AppControllerクラス用のファイルを作ります。⌘N(新規ファイル...)を選び、

Mac OS XのCocoaの中からObjective-C classを選択し、

このようにAppController.mとAppController.hをプロジェクト内に作成します。
まずはAppController.hを、最初に書いたメモを見ながら、一気に書いてみます。
#import <Cocoa/Cocoa.h>
@class Stopwatch;
@interface AppController : NSObject {
IBOutlet NSTextField *timeDisplay;
IBOutlet NSTableView *logView;
NSMutableArray *log;
NSTimer *timer;
Stopwatch *watch;
}
- (void)updateUI:(NSTimer *)t;
- (IBAction)pushStartStop:(id)sender;
- (IBAction)pushReset:(id)sender;
@end
メモの中で、AppControllerから出て行く矢印はインスタンス変数に対応していて、
逆にAppControllerに向かって来る矢印はメソッドに対応していますね。
これらのうち、IBOutletとIBActionを付けたものついてはインターフェースビルダー上で接続することになります。
このファイルを保存してから、MainMenu.xibを開いてください。インターフェースビルダーが起動します。

インターフェースビルダー上で、ライブラリからObjectを、MainMenu.xibウインドウへドラッグ&ドロップします。

ドロップしたObjectが選択された状態のまま、Identityパネルで、AppControllerを指定します。

次にWindowをこのようにデザインしましょう。

AppControllerからテーブルビュー、テキストフィールドへそれぞれ接続します。
StartボタンとResetボタンからそれぞれ、AppControllerへ接続します。

さてXcodeに戻って、AppController.mで実装です。
#import "AppController.h"
#import "Stopwatch.h"
@implementation AppController
- (void)updateUI:(NSTimer *)t
{
// Look at the watch
NSTimeInterval s = [watch second];
// Update UI
[timeDisplay setDoubleValue:s];
}
- (IBAction)pushStartStop:(id)sender
{
BOOL state = [sender state];
if (state) { // push Start
[watch startStop];
// Start the timer
timer = [NSTimer scheduledTimerWithTimeInterval:0.01
target:self
selector:@selector(updateUI:)
userInfo:nil
repeats:YES];
[timer retain];
}
else { // push Stop
[watch startStop];
[timer invalidate];
[timer release];
// Add to the log
NSString *newEntry = [[timeDisplay stringValue] copy];
[log addObject:newEntry];
[newEntry release];
[logView reloadData];
[logView scrollRowToVisible:[log count]-1];
}
}
- (IBAction)pushReset:(id)sender
{
[watch reset];
[self updateUI:nil];
}
- (id)init
{
[super init];
log = [[NSMutableArray alloc] init];
timer = nil;
watch = [[Stopwatch alloc] init];
return self;
}
- (void)dealloc
{
[log release];
[timer invalidate];
[timer release];
[watch release];
[super dealloc];
}
- (void)awakeFromNib
{
[self updateUI:nil];
}
@end
一気にやってしまいましたが、速すぎたかしら……
よくわからないところがたくさんあるかと思いますが
今回はざーっといってみましょう。
テーブルビュー関連のメソッドだけ、分けてやることにします。
テーブルビュー(NSTableView)は表形式の表示を担当するクラスで、
表示とユーザによる操作を扱うんですが、データ自身はよそに置きます。
テーブルビューは、データを取得したいときに、別のオブジェクトに問い合わせをして、
「テーブルは全部で何行?」「○行目の○列のデータは何?
など、聞いてくるんです。
こうすることによって、表のインターフェースとデータを完全に分離することができるわけです。
その問い合わせ先のオブジェクトを、テーブルビューではdataSourceと呼んでいます。インターフェースビルダー上で、

このように、テーブルビューのdataSourceアウトレットがAppControllerになるように接続してください。
テーブルビューはスクロールビュー
の中に配置されているので、設定パレットのタイトルに注意しながら、何度かクリックして、
"Table View"を確認してからctrlドラッグでAppControllerへ接続してください。
さてdataSourceへの問い合わせ用のメソッドは、
- (NSInteger)numberOfRowsInTableView:(NSTableView *)aTableView;
- (id)tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex;
この2つです。この2つをAppController.hに追加してください。
1つめが「テーブルは全部で何行?」の問い合わせで呼ばれます。
2つめは「○行目の○列のデータは何?」の問い合わせで呼ばれます。
メソッド名がおそろしく長いと感じられるかもしれませんが、まあそのうち慣れますよ。
テーブルビューを編集したい場合はもう1つメソッドがいるんですが、それはまた出てきたときに説明しましょう。
さてこいつらの実装です。AppController.mに、次を追加してください。
- (NSInteger)numberOfRowsInTableView:(NSTableView *)aTableView
{
return [log count];
}
- (id)tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex
{
return [log objectAtIndex:rowIndex];
}
メソッド名がおそろしく長いわりには、実装は1行だけですね・・・
インスタンス変数logは計測値を要素に持つ配列をさしているのでした。
[log count]はその配列の要素数です。つまり表の行数に等しいです。
[log objectAtIndex:rowIndex]は配列の第rowIndex番目の要素です。つまり表の第rowIndex行目のデータに相当します。
というわけで、これでdataSourceとしての役割を完璧に果たせるわけです。
AppControllerの実装は以上です。次はStopwatchクラスにいきましょう。
AppControllerのときと同様に、Stopwatch.mとStopwatch.mを作成してください。そしてStopwatch.hを、
#import <Foundation/Foundation.h>
@interface Stopwatch : NSObject {
NSTimeInterval offsetTime;
NSDate *startTime;
BOOL isBusy;
}
@property(readonly) NSTimeInterval second;
@property BOOL isBusy;
- (void)startStop;
- (void)reset;
@end
Stopwatch.mを、次のように。
#import "Stopwatch.h"
@implementation Stopwatch
@synthesize isBusy;
- (NSTimeInterval)second
{
NSTimeInterval second;
second = -[startTime timeIntervalSinceNow]; // zero if startTime is nil
second += offsetTime;
return second;
}
- (void)startStop
{
if (isBusy) {
// STOP
offsetTime = self.second;
[startTime release];
startTime = nil;
self.isBusy = NO;
}
else {
// START
startTime = [[NSDate alloc] init];
self.isBusy = YES;
}
}
- (void)reset
{
self.isBusy = NO;
offsetTime = 0.0;
}
- (id)init
{
[super init];
[self reset];
return self;
}
- (void)dealloc
{
[startTime release];
[super dealloc];
}
@end
最後に、インターフェースビルダー上で、次の2つを設定します。


こいつらはCocoaバインディングというしかけで、異なるオブジェクトのデータを同期させたいときに、
退屈なコードを書かずにすませられるんです。
Resetボタンはストップウォッチ休止中は操作可能で、計測中は操作禁止にしたい。
Progress Indicatorはストップウォッチ計測中だけ表示させてくるくる回したい。
上のように設定すれば、コードがまったく要らないんです。
さて、Xcodeで、ビルド&実行してみてください。動くかしら・・・どきどき・・・