ストップウォッチを作ろう(3) - Cocoaプログラミング

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で、ビルド&実行してみてください。動くかしら・・・どきどき・・・