第06講 ジャスチャーフラッシュ
印刷を御希望の方はページ一番下の「印刷用ページ」より印刷してください。
PDFファイルとして保存したい場合も「印刷用ページ」より保存できます。
見出しに「★」マークがついた箇所は説明動画がございます。
★第6講のポイント
- タッチジェスチャー(スワイプ・回転・ピンチ)検知の手法
- タイマーの利用と時間の計測
- NSLogによるデバッグメッセージの表示
はじめに
前回の「クイズ」では、初めて画面遷移を用いたアプリを構築しました。また、外部ファイルの読み込みや乱数の発生、可変配列等も扱いました。
第6講の課題アプリは「ジェスチャーフラッシュ」となります。今回より、iOS SDKの提供する各種機能の応用例を中心に解説していきます。このアプリは、iOSの特徴とも言える、タッチジェスチャーとゲームを組み合わせたものとなります。ユーザーはゲーム開始と同時に、ランダムに表示されるジェスチャー(スワイプ・回転・ピンチ)を30個分こなします。その際の所要時間を競います。上位タイムは「ハイスコア」として記録されます。
それでは、早速開発を始めて行きましょう!
プロジェクトの立ち上げと設定
新しいアプリを作る際は、まず、新規プロジェクトを立ち上げとアプリの設定を行います。
新規プロジェクトの立ち上げ
第1講と同じ要領で、新規プロジェクトを立ち上げます。その際、使用するテンプレートは「Single View Application」となります。
Product Name | GestureFlash |
Company Identifier | com.rainbowapps |
Device Family | iPhone |
Use Storyboard | チェックを入れる |
Use Automatic Reference Counting | チェックを入れる |
Include Unit Tests | チェックを入れる
|
アプリの設定
新規プロジェクトを立ち上げたら、アプリの設定を行います。ここで、アプリのアイコンやアプリの表示タイトル(Bundle Display Name)、サポートするデバイスの向き(Device Orientation)を以下のように、設定します。
その前に、今回使用する外部素材ファイルを一気にインポートしてしまいましょう。
今回使用する素材は以下の通りです。
icon.png | アイコン画像 |
start.png | 「開始」ボタン用画像 |
back.png | 「タイトルへ戻る」ボタン用画像 |
start-bg.png | スタート画面用背景画像 |
finish-bg.png | 結果表示画面背景画像 |
swipe-up.png | 「上スワイプ」画像 |
swipe-down.png | 「下スワイプ」画像 |
swipe-left.png | 「左スワイプ」画像 |
swipe-right.png | 「右スワイプ」画像 |
pinch-in.png | 「内向きピンチ」画像 |
pinch-out.png | 「外向きピンチ」画像 |
rotate-left.png | 「反時計回り回転」画像 |
rotate-right.png | 「時計回り回転」画像 |
アイコン設定とサポートするデバイスの向きの設定
素材のインポートが完了したら、Project Editorを開き、アイコンをRetina Displayと書かれたエリアの上にドラッグします。
サポートするデバイスの向きとアプリの名前は以下の通り設定します。
Bundle display nameの設定
アプリの表示タイトルを決めている設定項目「Bundle Display Name」を変更します。Project Editorの「info」タブをクリックして「Bundle Display Name」の項目を次のように変更していきます。
Bundle Display Name | Gesture Flash |
画面のデザイン
アプリの設定が完了したら、画面のデザインを行なっていきます。今回は3つの画面(スタート画面・プレー画面・結果表示画面)を持つアプリを作成します。Storyboardを開き、以下の手順に従って画面のデザインを行いましょう。
Autolayoutの無効化
画面レイアウトを3.5インチ(iPhone4S以前)と4インチ(iPhone5以降)に対応させるための設定を行います。
まずはXcode4.5からデフォルトで有効になっているAutolayoutを無効にします。
※Autolayoutはまだ不安定ですので本講座はAutolayoutを無効にして画面デザインを行っていきます。
下記のように①View Controller全体を選択し、②「File Inspector」に変更して、③「Use Autolayout」のチェックを外してください。
スタート画面のデザイン
まず、スタート画面からデザインしていきます。初期状態で、1画面分のViewは用意されているので、それをスタート画面とします。
背景画像の配置
ライブラリーエリアから「start-bg.png」を画面上にドラッグし、背景とします。
ラベル・ボタンの配置
次に以下の通り、3つのハイスコア表示用ラベルと1つの「スタート」ボタンを配置します。
異なる画面サイズの対応
3.5インチ及び4インチ画面にて画面のレイアウトが崩れないよう対応していきます。
まず下記画面上部のラベル要素を選択した状態で「Size Inspector」を選択しAutosizingの箇所を上の部分だけ赤色実践になるように変更します。これでラベル要素が上とセンター揃えで固定されました。
ボタン要素を選択してから「Size Inspector」を選択しAutosizingの箇所を下の部分だけ赤色実践になるように変更します。これでLabel要素が画面下とセンター揃えで固定されました。
プレー画面のデザイン
スタート画面のデザインが完了したら、プレー画面のデザインに移ります。先ほど述べたとおり、初期状態では1画面分のViewしか含んでいません。そこで、「クイズ」の時と同様、ViewをStoryboard上に新規追加します。ライブラリーエリアよりView Controllerを選び、Storyboard上に配置します。
ImageViewの配置
プレー画面では、背景を各ジェスチャーの画像とします、その画像を表示するために、画面全体にImage Viewを配置します。

ラベルの配置
次に画面上部にクイズのいくつかのラベルを配置します。まずは、以下のとおり、静的なラベル(コードから変更を受けないラベル)を配置します。
静的なラベルの配置が終わったら、コードから変更受ける動的なラベルを配置します。今回配置するのは、経過時間を示すラベルと、こなしたジェスチャーの数を示すラベルです。
異なる画面サイズの対応
3.5インチ及び4インチ画面にて画面のレイアウトが崩れないよう対応していきます。
まず下記画面上部のImageView要素を選択した状態で「Size Inspector」を選択しAutosizingの箇所を全て赤色実線をなしにしてください。これにより中央で固定にする事ができます。
結果表示画面のデザイン
3つの画面のうち、最後の結果表示画面のデザインに移ります。プレー画面同様、結果表示画面用のViewをStoryboard上に新規追加します。
背景画像の配置
スタート画面同様、まずライブラリーエリアより「finish-bg.png」を選択し、配置します。
ラベル・ボタンの配置
次に、以下のとおり、2つのラベルと1つの「スタート画面に戻る」ボタン(back.png)を配置します。ラベルに関して、1つは所要時間(記録タイム)を表示するものです。もう一つは、記録がハイスコアか否かを示すラベルで、ハイスコアを更新した場合、表示されます。
異なる画面サイズの対応
3.5インチ及び4インチ画面にて画面のレイアウトが崩れないよう対応していきます。
まず下記画面上部のラベル要素を選択した状態で「Size Inspector」を選択しAutosizingの箇所を上の部分だけ赤色実践になるように変更します。これでラベル要素が上とセンター揃えで固定されました。
ボタン要素とPicker View要素を選択してから「Size Inspector」を選択しAutosizingの箇所を下の部分だけ赤色実践になるように変更します。これでLabel要素が画面下とセンター揃えで固定されました。
これにて、画面のデザインは完了となります。
新規View ControllerのアサインとSegueの設定
今回も「クイズ」同様、それぞれの画面に対して新規にView Controllerを作成します。また、各画面間のSegueを設定します。
新規View Controllerの作成
まずは、プレー画面用のView Controllerクラスから作成します。ナビゲーターエリアの中にある、「GestureFlash」と書かれたフォルダを右クリックし、「クイズ」の時と同じ手順で新規のView Controllerを作成します。Cocoa Touchの中から、Objective-C classを選択してください。
以下の表の通り、クラス名とオプションを設定し、「Next」をクリックし、既存のView Controllerとフォルダに保存します
Class | PlayViewController |
Subclass of | UIViewController |
Targeted for iPad | チェックを外す |
With XIB for user interface | チェックを外す |
同様の手順で、結果表示画面用のView Controllerクラスも作成します。以下の表の通り、クラス名とオプションを設定します。
Class | ResultViewController |
Subclass of | UIViewController |
Targeted for iPad | チェックを外す |
With XIB for user interface | チェックを外す |
ここまでの手順を正しく行った場合、ナビゲーターエリアに合計4つのファイルが新たに作成されたはずです。
新規View Controllerのアサイン
ここまでの手順で、3つの画面用のView Controllerのひな形が作成されました。次に、新たに作ったPlay View ControllerとResult View Controller をそれぞれのViewにアサインしなければいけません。
「クイズ」の時と同様、Interface Builderからそれぞれの画面のView Controllerを選択します。
Storyboard Segueの設定
Storyboard Segueは、遷移元の画面と遷移先の画面を「線」で結ぶことによって設定します。今回は、以下の3つの画面遷移があるので、それぞれに対してStoryboard Segueを設定していきます。なお、Segueの種類は全て「Modal」を選択します。
- 「スタート」ボタン → Play View Controller
- Play View Controller → Result View Controller
- 「タイトルへ戻る」ボタン → View Controller
お気づきかもしれませんが、この流れは前回作成した「クイズ」と全く同じです。詳しい手順がわからない場合は、前回のテキストを参照してください。すべて正しく行えた場合は以下のとおりすべての画面が線で繋がります。
また、Document Outline画面から、Segueの状態を確認することもできます。DocumentOutlineはInterface Builderの左手にある以下の画面です。

この画面が万が一表示されていない場合は、Interface Builderの左下の角にある、以下のボタンをクリックします。
このDocument Outline上で、各Segueがリストに表示されているのを確認して下さい。このとき、ボタンに設定したSegueと、直接View Controllerに設定したSegueでは表示が異なることを確認して下さい。
最後に、ボタンに紐付いていない、Play View ControllerからResult View ControllerへのSegueに識別子をつけます。その識別子は「toResultView」とします。
スタート画面のコーディング
ここでは、スタート画面のView Controllerのコーディングを行います。
メンバー変数の定義
以下のとおり、メンバー変数をViewController.mに宣言します。
ViewController.m
@implementation ViewController { //ハイスコア用のラベル IBOutlet UILabel *highscore1_label; IBOutlet UILabel *highscore2_label; IBOutlet UILabel *highscore3_label; } |
User Defaults領域の参照
User Defaultsとは、すべてのiOSアプリが持つ、データ保存領域です。基本的に、様々データを「キー(キーワード)」と紐付けることで、簡単に保存できます。このデータはアプリが終了しても、しっかり残るので、ゲームの途中経過やユーザーの設定を保存するのに最適です。今回は、ハイスコアを記録するのに使います。
スタート画面では、ハイスコアの読み出しを行います。もしハイスコアが記録されている場合は画面上のラベルの値をそれに応じて更新します。今回User Defaultには、以下のとおり3つのデータの保存と参照をします。
キー | データの種類 |
high_score1 | ハイスコア1位の記録 |
high_score2 | ハイスコア2位の記録 |
high_score3 | ハイスコア3位の記録 |
User Defaultsの参照は非常に簡単です。基本的には、NSUserDefaultsクラスのメソッド1つで、指定したキーの値を変数に代入することができます。以下の通り「-(void)viewDidLoad」メソッドを書き換えて下さい。
ViewController.m
- (void)viewDidLoad { [super viewDidLoad]; //User Defaultsへアクセスする NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; //1位から3位までのハイスコアを取得し、double型の変数に格納 double highscores1 = [defaults doubleForKey:@"highscore1"]; double highscores2 = [defaults doubleForKey:@"highscore2"]; double highscores3 = [defaults doubleForKey:@"highscore3"];
//NSLogによるデバッグメッセージ NSLog(@"ハイスコア: 1位-%f 2位-%f 3位-%f", highscores1,highscores2, highscores3); //ハイスコアの存在を確認 //もし、ハイスコアが存在する場合(0でない場合)は画面の一覧に表示 if (highscores1 != 0) { highscore1_label.text = [NSString stringWithFormat:@"%.3f 秒", highscores1]; } if (highscores2 != 0) { highscore2_label.text = [NSString stringWithFormat:@"%.3f 秒", highscores2]; } if (highscores3 != 0) { highscore3_label.text = [NSString stringWithFormat:@"%.3f 秒", highscores3]; } } |
User Defaultsを読み出す際、今回は読み出し結果をdouble型の変数として返す「doubleForKey」メソッドを使っています。もしキーに該当する値が存在しない場合は「0」が返されます。この他にも、様々データ型で読みだし結果を返すメソッドが用意されているので、適宜、Appleのドキュメント等を参考にして、最も適当なものを選んで下さい。
NSLogによるデバッグメッセージの表示
コーディングを行う際、正常な動作を確かめる上で、変数の状態や処理の内容を見る必要が出てきます。
例えば、今回の例で、ハイスコアのラベルの値が変更されないというバグに遭遇したとします。その場合、User Defaultsのデータ読み出しの段階で問題が起きているのか、ラベルの値を更新する処理で問題があるのかを、切り分ける必要があります。このような時、NSLogによるデバッグは非常に有効となります。
使い方は非常に簡単です。コードの中に以下の一文を加えるだけです。
//NSLogによるデバッグメッセージ NSLog(@"ハイスコア: 1位-%f 2位-%f 3位-%f", highscores1,highscores2, highscores3); |
この例では、ハイスコアを格納するdouble型の変数の値を出力しています。基本的な使い方はNSStringでの「stringWithFormat」メソッドと同じです。前半で、表示する文字列の書式を指定し、後半に参照する変数を指定します。書式を持った文字列の作成に関しては、第2講を参照してください。
これにて、View Controllerの実装は完了となります。
結果表示画面のコーディング
「クイズ」の時と同様、プレー画面から結果表示画面へ遷移する際、データの受け渡しを行う都合上、次は、結果表示画面のコーディングを行なっていきます。
Result View Controllerのメンバー変数
まずはメンバー変数から宣言していきます。以下のとおり、メンバー変数を宣言してください。
ResultViewController.m@implementation ResultViewController { //各種ラベル IBOutlet UILabel *timeLabel; IBOutlet UILabel *newHighScoreLabel; } |
ここにある、NSTimeIntervalは、精密な経過時間を扱うことのできるクラスです。これを用いることによって、マイクロ秒単位で時間の計測ができます。その使い方は後ほど説明します。
プロパティーの設定とプロトタイプの宣言
今回、Result View Controllerのメンバー変数の「time」は、プレー画面から受け渡される値を格納するためのものです。その際、Play View Controllerから、この「time」を参照する必要があります。外部クラスからメンバー変数をアクセスするためのプロパティーを設定する必要があります。
以下のとおり、ResultViewController.hに記述してください。
ResultViewController.h@interface ResultViewController : UIViewController @property NSTimeInterval time; @end |
ハイスコアの計算User Defaults領域への書き込み
結果表示画面では、呼び出された段階で、「_time」にゲームの所要時間(記録タイム)が格納されています。
まずはその値を元に、既存のハイスコアと比較をし、上回った場合は書き換えを行うメソッドを実装します。
以下のとおり、ResultViewController.mにコードを記述します。
ResultViewController.m -(void) checkHighScore { //ハイスコアが更新されかどうかの管理(スコア比較前は「偽」に) bool newHighScore = false; //一度「ハイスコア更新」ラベルを非表示 newHighScoreLabel.hidden = true; //User Defaultsへアクセスする NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; //1位から3位までのハイスコアを取得し、double型の変数に格納 double highscores1 = [defaults doubleForKey:@"highscore1"]; double highscores2 = [defaults doubleForKey:@"highscore2"]; double highscores3 = [defaults doubleForKey:@"highscore3"]; //(全てのハイスコアが既にある場合)比較の結果、今回のtimeが当てはまる順位に記録を挿入 //1位より早い場合 if (highscores1 != 0 && _time <= highscores1) { highscores3 = highscores2; highscores2 = highscores1; highscores1 = _time; newHighScore = true; //2位より早い場合 } else if (highscores2 != 0 && _time <= highscores2) { highscores3 = highscores2; highscores2 = _time; newHighScore = true; //3位より早い場合 } else if (highscores3 != 0 && _time <= highscores3) { highscores3 = _time; newHighScore = true; } //ハイスコアがまだ格納されていない場合の_timeとの比較 //1位がまだない場合 else if (highscores1 == 0) { highscores1 = _time; newHighScore = true; //2位がまだなく、1位より遅い場合 } else if (highscores2 == 0 && _time >= highscores1) { highscores2 = _time; newHighScore = true; //3位がまだなく、2位より遅い場合 } else if (highscores3 == 0 && _time >= highscores2) { highscores3 = _time; newHighScore = true; } //新しいハイスコアをUser Defaultsに保存 [defaults setDouble:highscores1 forKey:@"highscore1"]; [defaults setDouble:highscores2 forKey:@"highscore2"]; [defaults setDouble:highscores3 forKey:@"highscore3"]; //もし、ハイスコアが更新された場合は「ハイスコア更新」ラベルを表示 if (newHighScore == true) { newHighScoreLabel.hidden = false; } } |
ここでは、User Defaultsの値の読み出しを行い、それらを今回の記録である「_time」と比較しています。なお、「_time」はNSTimeIntervalのインスタンスですが、double型の変数としても扱えます。比較を行ったあと、最新のハイスコアをUser Defaultsに書き込みます。なお、「ハイスコア更新」ラベルですが、比較前で非表示とします。比較後に、もしハイスコアが更新されるようなことがあれば、表示状態にします。
Result View Controllerの初期処理
ハイスコアの更新を行うメソッドの実装が終わったら、初期処理を実装します。以下のとおり、ResultViewController.mの「viewDidLoad」を実装します。
ResultViewController.m - (void)viewDidLoad { [super viewDidLoad]; //タイムを表示 timeLabel.text = [NSString stringWithFormat:@"%.3f 秒", _time]; //ハイスコアの判定 [self checkHighScore]; } |
ここでは、_timeの値をラベルに反映し、先程実装したハイスコアの更新処理を呼び出しています。
プレー画面のコーディング
次に、本アプリの中心となるプレー画面のコーディングを行なっていきます。
タッチジェスチャーとは?
まず、タッチジェスチャーに関する説明を行なっていきます。昨今のスマートフォンやタブレットは基本的にタッチパネルで操作しますが、これらはページをめくる動作やスクロール動作などを直感的にタッチで行えます。これらをタッチジェスチャーといいます。iOSデバイスでは一般的なスワイプ(1本指でスクロール)をはじめ、回転(2本指で画面上の写真を回転)、ピンチ(拡大・縮小)などのジェスチャーを非常に簡単に検知できるようになっています。
今回はこの中でも、Swipe・Pinch・Rotateの3タイプに着目します。これら3タイプのジェスチャーを30個分、ランダムにユーザーに提示し、それをすべてやり終えるまでの時間を竸います。
メンバー変数の宣言
まずは、メンバー変数の宣言から行なっていきます。
以下のとおり、PlayViewController.mを編集して下さい。
PlayViewController.m@implementation PlayViewController { //ゲームの経過時間を計測 NSDate *startTime; //こなしたジェスチャーの数を管理 int completedGestures; //現在の問題で、発見すべきジェスチャーを記録 int currentGesture; //各種ラベル IBOutlet UILabel *timeLabel; IBOutlet UILabel *completedGesturesLabel; //ジェスチャーの画像を表示するImage View IBOutlet UIImageView *gestureImage; //経過時間を画面に表示するためのタイマー NSTimer* timer; double timerCount; } |
ここにある、NSDateは、時間を取り扱うためのクラスです。その扱いかたは、後ほどのNSTimeIntervalと併せて解説します。また、NSTimerはタイマーによる割り込み処理を行うためのクラスです。これにより、指定した時間間隔毎に、任意のメソッドを呼び出すことができます。
経過時間の計測とResult View Controllerへの遷移
まずは、時間の経過時間の測定に関する説明をします。その前に、以下のとおり、PlayViewController.mを編集して下さい。
PlayViewController.m//結果表示画面へのSegueの発動 - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{ //所要時間を計測 NSTimeInterval elapsedTime = [startTime timeIntervalSinceNow]; elapsedTime = -(elapsedTime); //ResultViewController(RVC)のインスタンスを作成し、 //RVCクラスのメンバー変数である「correctPercentage」に値を渡す if ([[segue identifier] isEqualToString:@"toResultView"]) { ResultViewController *rvc = (ResultViewController*)[segue destinationViewController]; rvc.time = elapsedTime; } } |
これは、「クイズ」でも取り扱った、Segueを発動させるメソッドです。ゲームの経過時間(記録タイム)を測定する場合、ゲームが開始された時間から、完了までの時間を計測することになります。そこで今回は、ゲーム完了時にSegueを発動させるようにし、そのタイミングで所要時間を計測します。
経過時間を得るためには以下のように、NSTimeIntervalとNSDateのインスタンスを組み合わせます。
//所要時間を計測 NSTimeInterval elapsedTime = [startTime timeIntervalSinceNow]; |
ここにある「startTime」は後ほど、初期処理を実装する際に、セットします。NSDateクラスのインスタンスに対して、「timeIntervalSinceNow」を呼び出すと、現在までの経過時間をマイクロ秒単位で得ること事ができます。その際、「開始時刻」から「現在時刻」が差し引かれるので、経過時間は負の数となります。そこで、実経過時間を得るためには、以下のようにする必要があります。
elapsedTime = -(elapsedTime); |
経過時間を取得した後は、それをResult View Controllerに渡します。その手法は「クイズ」の時と全く同じです。その際、PlayViewController.mに以下の記述をする必要があります。
PlayViewController.h#import <UIKit/UIKit.h> #import "ResultViewController.h" |
次のジェスチャーを提示するメソッド
今回は、ランダムに提示された30個のジェスチャーをこなすことがゲームの目的となります。そこで、指定された1つのジェスチャーが正しく検知された場合、次のジェスチャーを提示するメソッドを実装していきます。以下に示すように、PlayViewController.mを編集して下さい。
PlayViewController.m//次の問題を表示 -(void)nextProblem { //もし出題規定数(ジェスチャー30個)に達している場合 if (completedGestures == 30) { //結果表示画面へのSegueを始動 [self performSegueWithIdentifier:@"toResultView" sender:self]; //メソッドの処理を中断 return; }
//配列にジェスチャーを示す画像取り込み UIImage *gestureIcons[8]; gestureIcons[0] = [UIImage imageNamed:@"swipe-right.png"]; gestureIcons[1] = [UIImage imageNamed:@"swipe-left.png"]; gestureIcons[2] = [UIImage imageNamed:@"swipe-up.png"]; gestureIcons[3] = [UIImage imageNamed:@"swipe-down.png"]; gestureIcons[4] = [UIImage imageNamed:@"pinch-in.png"]; gestureIcons[5] = [UIImage imageNamed:@"pinch-out.png"]; gestureIcons[6] = [UIImage imageNamed:@"rotate-right.png"]; gestureIcons[7] = [UIImage imageNamed:@"rotate-left.png"]; //乱数をもとに、次のジェスチャーを選択 srand((unsigned int)time(0)); currentGesture = rand() % 8; NSLog(@"got new gesture current: %d", currentGesture); //画面に出てるジェスチャーの画像を差し替え、問題番号を更新 gestureImage.image = gestureIcons[currentGesture]; completedGesturesLabel.text = [NSString stringWithFormat:@"%d", completedGestures]; } |
基本的に、このメソッドでは、これまでにこなしたジェスチャーの数をもとに、次の画面へのSegueを発動するか、次の問題を提示するかを判断します。次の問題を提示するとなった場合、ランダムにジェスチャーが指定されます。
今回は全部で3タイプ・合計8種類のジェスチャーを扱うものとし、それぞれに識別番号を指定します。ます。それらを以下に示します。
種類 | 識別番号 | 画像 |
右スワイプ | 0 | swipe-right.png |
左スワイプ | 1 | swipe-left.png |
上スワイプ | 2 | swipe-up.png |
下スワイプ | 3 | swipe-down.png |
内向きピンチ | 4 | pinch-in.png |
外向きピンチ | 5 | pinch-out.png |
時計回り回転 | 6 | rotate-right.png |
反時計回り回転 | 7 | rotate-left.png |
これらをランダムに指定し、識別番号を「currentGesture」に代入します。その後、画面に出ているラベルやImage Viewを適宜更新します。
ジェスチャーの認識
今回は、ジェスチャーの認識を行うのですが、そのための手段としてiOS SDKでは、UIGestureRecognizerというものを提供しています。これを利用することによって、簡単にジェスチャーを認識することができます。
プロトコルの設定
それでは、まずその準備からはじめます。以下の通り、PlayViewController.hにプロトコルの設定を施します。
PlayViewController.h 【変更前】 @interface PlayViewController ↓ 【修正後】 @interface PlayViewController : UIViewController <UIGestureRecognizerDelegate> |
UIGestureRecognizerのセット
プロトコルの設定が終わったら、UIGestureRecognizerのセットを行います。以下のとおり、PlayViewController.mを編集して下さい。
PlayViewController.m//Gesture Recognizerをセット -(void)setGestureRecognizers { //Pinch(2本の指でつまむ)の認識 UIPinchGestureRecognizer *pinchRecognizer = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(pinchDetected:)]; [self.view addGestureRecognizer:pinchRecognizer]; //Rotate(2本の指で回転)の認識 UIRotationGestureRecognizer *rotationRecognizer = [[UIRotationGestureRecognizer alloc] initWithTarget:self action:@selector(rotationDetected:)]; [self.view addGestureRecognizer:rotationRecognizer]; //右向きのSwipe(1本指でなぞる)の認識 UISwipeGestureRecognizer *swipeRightRecognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipeRightDetected:)]; swipeRightRecognizer.direction = UISwipeGestureRecognizerDirectionRight; [self.view addGestureRecognizer:swipeRightRecognizer]; //左向きのSwipe(1本指でなぞる)の認識 UISwipeGestureRecognizer* swipeLeftRecognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipeLeftDetected:)]; swipeLeftRecognizer.direction = UISwipeGestureRecognizerDirectionLeft; [self.view addGestureRecognizer:swipeLeftRecognizer]; //上向きのSwipe(1本指でなぞる)の認識 UISwipeGestureRecognizer *swipeUpRecognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipeUpDetected:)]; swipeUpRecognizer.direction = UISwipeGestureRecognizerDirectionUp; [self.view addGestureRecognizer:swipeUpRecognizer]; //下向きのSwipe(1本指でなぞる)の認識 UISwipeGestureRecognizer* swipeDownRecognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipeDownDetected:)]; swipeDownRecognizer.direction = UISwipeGestureRecognizerDirectionDown; [self.view addGestureRecognizer:swipeDownRecognizer]; } |
ここにあるコードは非常に長く、暗号文のように思うかもしれませんが、これら一連のコードのよって、それぞれのジェスチャーを認識する準備が整います。
スワイプの認識
スワイプは前述したとおり、1本の指で上下左右になぞるジェスチャーです。これらは方向ごとにそれぞれ独立したUIGestureRecognizerをセットします。
UIGestureRecognizerがセットされた後、iOSデバイスがジェスチャーを認識し場合、次のメソッドが呼ばれます。PlayViewController.mに追記して下さい。
PlayViewController.m //右向きスワイプ検知時に呼ばれるメソッド - (IBAction)swipeRightDetected:(UIGestureRecognizer *)sender { NSLog(@"右向きSwipe"); NSLog(@"current: %d", currentGesture); //正解が右向きSwipe(0番)なら if (currentGesture == 0) { NSLog(@"NEXT"); completedGestures++; [self nextProblem]; } }
//左向きスワイプ検知時に呼ばれるメソッド - (IBAction)swipeLeftDetected:(UIGestureRecognizer *)sender { NSLog(@"左向きSwipe"); NSLog(@"current: %d", currentGesture); //正解が左向きSwipe(1番)なら if (currentGesture == 1) { NSLog(@"NEXT"); completedGestures++; [self nextProblem]; } }
//上向きスワイプ検知時に呼ばれるメソッド - (IBAction)swipeUpDetected:(UIGestureRecognizer *)sender { NSLog(@"上向きSwipe"); NSLog(@"current: %d", currentGesture); //正解が上向きSwipe(2番)なら if (currentGesture == 2) { NSLog(@"NEXT"); completedGestures++; [self nextProblem]; } }
//下向きスワイプ検知時に呼ばれるメソッド - (IBAction)swipeDownDetected:(UIGestureRecognizer *)sender { NSLog(@"下向きSwipe"); NSLog(@"current: %d", currentGesture); //正解が下向きSwipe(3番)なら if (currentGesture == 3) { NSLog(@"NEXT"); completedGestures++; [self nextProblem]; } } |
これらは、上下左右それぞれの方向のスワイプを検知した場合に呼ばれます。その際、「currentGesture」のがチェックされ、指定されたジェスチャーと検知したジェスチャーが一致した場合は、次のジェスチャーを提示する「nextProblem」が呼ばれます。回転の認識
回転は、2本の指で画面上を回転するジェスチャーを指します。そのジェスチャーを検知した場合に呼ばれるメソッドを、以下のとおり記述して下さい。
PlayViewController.m//回転動作検知時の呼ばれるメソッド - (IBAction)rotationDetected:(UIGestureRecognizer *)sender { //Rotate開始時から見た回転の度合い(ラジアン) CGFloat radians = [(UIRotationGestureRecognizer *)sender rotation]; //「ラジアン」を「度」に変換 CGFloat degrees = radians * (180/M_PI); if (degrees > 90) { NSLog(@"時計回りにRotate degrees: %f", degrees); NSLog(@"current: %d", currentGesture); //正解が時計回りRotate(6番)なら if (currentGesture == 6) { NSLog(@"NEXT"); completedGestures++; [self nextProblem]; } } else if (degrees < -90) { NSLog(@"反時計回りにRotate degrees: %f", degrees); NSLog(@"current: %d", currentGesture); //正解が時計回りRotate(7番)なら if (currentGesture == 7) { NSLog(@"NEXT"); completedGestures++; [self nextProblem]; } } } |
この時、最初に2本の指が画面に置かれた場所を基準とし、少しでも回転を検知した場合、回転した相対量が「ラジアン」の数値として返されます。まず、「ラジアン」を「度」に変換する必要があります。その際、以下の計算式を用います。
degrees = radians * (180/M_PI); |
相対的な回転量が90度を超えた場合、ジェスチャーがこなされたというようにします。この時、スワイプ同様「currentGesture」の値と比較され、提示されたジェスチャーと認識したジェスチャーが一致した場合、「nextProblem」が呼ばれます。
なお、開始位置から右(時計回り)方向に回転された場合、正の値が返されます。開始位置から左(反時計回り)方向に回転された場合、負の値が返されます。
ピンチの認識
次に、ピンチの認識を行います。画面上で2本の指を近づけたり遠ざけたりするジェスチャーです。そのジェスチャーを検知した場合に呼ばれるメソッドを、以下のとおり記述して下さい。
PlayViewController.m//ピンチ動作検知時に呼ばれるメソッド - (IBAction)pinchDetected:(UIGestureRecognizer *)sender { //ピンチ開始の2本の指の距離を1とした時 //現在の2本の指の相対距離 CGFloat scale = [(UIPinchGestureRecognizer *)sender scale]; if (scale > 2.4) { NSLog(@"外向きにPinch scale: %f", scale); NSLog(@"current: %d", currentGesture); //正解が外向きPinch(5番)なら if (currentGesture == 5) { NSLog(@"NEXT"); completedGestures++; [self nextProblem]; } } else if (scale < 0.4) { NSLog(@"内向きにPinch scale: %f", scale); NSLog(@"current: %d", currentGesture); //正解が内向きPinch(4番)なら if (currentGesture == 4) { NSLog(@"NEXT"); completedGestures++; [self nextProblem]; } } } |
この時、最初に2本の指が画面に置かれた時の、2本の指の距離が「1」となります。同じ直線上で少しでも距離が変化した場合、変化の相対量が数値が返されます。例えば、「1以上の数値」が返された場合は、2本の指が離れていることになります。一方、「1未満の数値」が返された場合は、2本の指が近づいていることになります。
今回は、相対距離が「2.4」を上回った場合、外向きのピンチがこなされたとします。同様に、相対距離が「0.4」を下回った場合、内向きのピンチがこなされたとします。ピンチが正常にこなされたと認識できた場合、「currentGesture」の値と比較され、正解の場合は「nextProblem」が呼ばれます。
これにて、ジェスチャーの検知に関するコードの記述は完成となります。
Play View Controllerの初期処理
次に、Play View Controllerの初期処理を行います。以下の通り「viewDidLoad」を編集します。
- (void)viewDidLoad { [super viewDidLoad]; //こなしたジェスチャーの数を0にリセット completedGestures = 0; //Gesture Recognizersをセット [self setGestureRecognizers]; //最初の問題を表示 [self nextProblem]; //開始時間を記録 startTime = [NSDate date]; } |
ここで着目すべきは、「startTime」の値が現在の時刻にセットされているところです。冒頭で説明したように、これにより、30個のジェスチャーをこなすまでの所要時間(タイム記録)を測定することができます。
NSTimerによるタイマー割り込み処理
次に、よく使う機能としてNSTimerがあります。このNSTimerはタイマー割り込み処理を提供するクラスです。NSTimerが発動されると、指定された間隔毎割り込みが発生し、指定されたメソッド呼ばれます。今回はこれを用いて、ゲーム画面中に経過時間の概算値を表示する機能を実装します。まず、PlayViewController.mの「viewDidLoad」を以下の通り編集します。
PlayViewController.m-(void)viewDidLoad { 【中略・以下追記】 //経過時間を表示するタイマーの始動 //0.1秒毎に「-(void)onTimer」が呼ばれる timerCount = 0; timer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(onTimer:) userInfo:nil repeats:YES]; } |
さらに、0.1秒毎に呼ばれる「onTimer」というメソッドを実装します。
PlayViewController.m//0.1秒毎に呼ばれる経過時間表示を更新処理 -(void)onTimer:(NSTimer*)timer { timerCount = timerCount + 0.1; timeLabel.text = [NSString stringWithFormat:@"%.1f", timerCount]; } |
NSTimerが発動されると、指定時間間隔(今回は0.1秒)毎に「onTimer」が呼ばれます。そして、「onTimer」内で経過時間を示すラベルの値に「0.1」を足します。これにより、ユーザーはゲーム中に概算経過時間を知ることができます。
UI画面とコードの関連付け
ここまでで、基本的にコードの記述は全て完了となります。そこで、これまでに設置したUI画面上の部品とコード上の処理を関連付けます。
スタート画面の関連付け
スタート画面では、3つのハイスコアを示すラベルをそれぞれ関連付けます。
プレー画面の関連付け
プレー画面では、経過時間とこなしたジェスチャーの数を示す2つのラベルを関連付けます。また、ジェスチャーの画像を表示するためのImage Viewを関連付けます。
結果表示画面の関連付け
結果表示画面では、記録タイムと「ハイスコアを更新」を示す2つのラベルを関連付けます。

ビルドと動作試験
これにて、すべての作業は完了となります。編集内容を全て保存し、ビルドを行なってください。このテキストの内容をすべて正しくやった場合、特に問題なくアプリが動作するはずです。
ここで、一点注意があります。iOSシミュレーター上では、マウスを使う都合上、ジェスチャー入力に限界があります。ジェスチャーの動作確認を行う際は、実機での検証を強く勧めます。
プレー画面で、3タイプ・8種類のジェスチャーがしっかり認識できること、NSTimerによって、概算経過時間が表示されることを確認します。また、結果表示画面では記録タイムが表示されること、また、スタート画面上でハイスコアが正常に表示されることも確かめて下さい。
デバッグメッセージの確認
「ジェスチャーフラッシュ」では、各所にNSLogを用いたデバッグメッセージを表示する命令を記述しています。ここでは、このNSLogによるデバッグメッセージの確認方法を解説します。
デバッグメッセージは、以下のように、デバッガーエリアに表示されます。
デバッガーエリアが表示されない場合は、Xcode画面の右上にある以下のアイコンが選択されていることを確認します。
このNSLogによるデバッグメッセージは非常に便利かつ、よく使う機能なので、覚えるようにして下さい。
まとめ
第6講では、iOS SDKにおけるタイマーによる割り込み処理や時間計測の手法を学びました。また、ジェスチャー入力の検知手法、および、NSLogを用いたデバッグメッセージの出力方法を扱いました。
今回扱ったタイマーによる割り込み処理やジェスチャー入力は、様々なアプリで応用されています。よく使う機能なので、ぜひ使い方をマスターして下さい。また、自分流にゲームをアレンジして、いろんな機能を付加して下さい。
次回からは、通信を行うアプリを取り扱っていきます。通信機能もよく使う要素のうちの1つなので、積極的に学んで行きましょう。