iOS (iPhone/iPad) アプリ開発でタイマーを使ってテーブルの項目を定期的に更新させる
TODO リストを表示するアプリとかで、予定開始時刻までの残り時間をリアルタイムに表示する、みたいなアプリを考える。UITableView に TODO リストの一覧を表示させると同時に、その TODO の残り時間をリアルタイムに表示させたい場合は、UITableView を定期的に更新してやる必要がある。これを UITableViewController, NSTimer, NSDate の組合せで作るとき、いくつかしょうもないことでハマったのでメモ。
下記は、1秒毎に残り時間を更新するという前提で書いている。
- UITableViewController のサブクラスでテーブルを強制的に更新させるには [self.tableView reloadData] する。
- [NSData date] 等で得られるインスタンスを保持するためには retain させる。また、retain したら release する。
- timer は 1 秒よりずっと短い間隔で呼びださないと、処理時間のずれによってときどき更新されてないように見える。
- 各項目の時間を同期させるためには、timer が呼びだす ticker 内で現在時刻を取得しておく必要がある。
- 選択状態を維持するためには、ticker 内で reloadData した後に自力で選択しなおす必要がある。
ソース例
iOS の Empty アプリケーションを作って、TimerTableController クラスを UITableViewController のサブクラスとして生成しておく。IB を全く使わないで作ってるので、storyboard や nib は生成する必要はない。
- AppDelegate.m
実装が必要なのは以下のメソッドのみ。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { self.window = [[[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]] autorelease]; // Override point for customization after application launch. TimerTableController *table = [[TimerTableController alloc] initWithStyle: UITableViewStylePlain ]; [self.window addSubview: table.tableView]; self.window.backgroundColor = [UIColor whiteColor]; [self.window makeKeyAndVisible]; return YES; }
一瞬 [self.window addSubview: table] としたくなるかもしれないけど、UITableViewController は View ではないのでできない。
- TimerTableController.h
#import <UIKit/UIKit.h> @interface TimerTableController : UITableViewController { NSTimer *timer; // タイマー NSDate *current; // 更新時間を保持する NSMutableArray *array;// 項目のデータを保持する NSInteger selected; // 選択されたセルの位置を記憶する } @end
- TimerTableController.m
冒頭で挙げたポイントを、コメントとして対応するコードに書いてみた。
#import "TimerTableController.h" @implementation TimerTableController - (id)initWithStyle:(UITableViewStyle)style { self = [super initWithStyle:style]; if (self) { // 0.1秒間隔で self の -ticker: を繰り返し呼びだす。 // 1 秒間隔にすると、タイマーの時間のずれによって、ときどき更新が飛んでいるように見えることがある timer = [NSTimer scheduledTimerWithTimeInterval:0.1f target: self selector:@selector(ticker:) userInfo: nil repeats: YES ]; array = [[NSMutableArray alloc] init]; // サンプルデータを作る。10秒後、20秒後・・・に予定時刻がくる TODO リストを作る for ( int i = 1; i < 20; i++ ) { NSDate *date = [[NSDate dateWithTimeIntervalSinceNow: i * 10] retain]; [array addObject: date]; } current = [[NSDate date] retain]; // 表示項目間の時刻ずれを防ぐために使う selected = -1; // 選択項目なし } return self; } // タイマーから呼びだされるメソッド -(void)ticker:(NSTimer*)timer { /* NSMutableArray *tmparr = [[NSMutableArray alloc] initWithCapacity:[array count]]; // 予定時刻が過ぎたときにリストから消去する場合は、ここのコメントアウトを外す // ただし、選択状態のセルがある場合に、セルの位置がずれたり消えたりする問題がおこるが // それには対処していない。 for ( NSDate *date in array ) { NSTimeInterval val = [date timeIntervalSinceNow]; if ( val < 0 ){ [tmparr addObject: date]; } } for ( NSDate *date in tmparr ) { [array removeObject: date]; } [tmparr release]; */ // 前回から秒の数値が変ってない場合は更新しない。 NSDate* tmp = [NSDate date]; if ( [tmp timeIntervalSinceDate: current] < 1.0f ) { return; } [current release]; current = [tmp retain]; // 表示項目間の時刻ずれを防ぐために使う [self.tableView reloadData]; // 強制的に更新する if ( selected >= 0){ // 選択状態を維持する NSIndexPath *index = [NSIndexPath indexPathForRow: selected inSection:0]; [self.tableView selectRowAtIndexPath: index animated:YES scrollPosition:UITableViewScrollPositionNone]; } } - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)viewDidLoad { [super viewDidLoad]; } - (void)viewDidUnload { for ( NSDate* date in array ) { [date release]; } [array release]; [current release]; [super viewDidUnload]; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; } - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; [timer fire]; } - (void)viewWillDisappear:(BOOL)animated { [timer invalidate]; [super viewWillDisappear:animated]; } - (void)viewDidDisappear:(BOOL)animated { [super viewDidDisappear:animated]; } - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { return (interfaceOrientation == UIInterfaceOrientationPortrait); } #pragma mark - Table view data source - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [array count]; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"Cell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; } // 現在時刻との差分を計算する NSDate *date = (NSDate*)[array objectAtIndex: [indexPath row]]; NSTimeInterval interval = [date timeIntervalSinceDate: current]; int val = (int)interval; // 現在時刻の差分と、予定時刻を表示させる if ( val > 0 ){ [cell.textLabel setText: [NSString stringWithFormat: @"%d: %@", val, [date description]]]; }else{ [cell.textLabel setText: [NSString stringWithFormat: @"done: %@", [date description]]]; } return cell; } - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { selected = [indexPath row]; // 選択されたセル番号を覚えておく。 } @end