ジェスチャーアプリ

講義の目標

  • UserDefaultを使った処理ができるようになること
  • タッチジェスチャーの取得処理を理解すること
  • タイマー処理の実装ができるようになること
  • デバッグの仕方(NSLog、デバッガ)をマスターすること

はじめに

今回の課題アプリは ジェスチャーフラッシュ となります。今回より、iOS SDKの提供する各種機能の応用例を中心に解説していきます。このアプリは、iOSの特徴とも言える、タッチジェスチャーとゲームを組み合わせたものとなります。ユーザーはゲーム開始と同時に、ランダムに表示されるジェスチャー(スワイプ・回転・ピンチ)を30個分こなします。その際の所要時間を競います。上位タイムは「ハイスコア」として記録されます。

完成品のサンプルは以下よりダウンロードできます。イメージが湧かない方は是非ご確認ください。

iOS完成品サンプル(ジェスチャーアプリ)

それでは、早速開発を始めて行きましょう!

プロジェクトの立ち上げと設定

新しいアプリを作る際は、まず、新規プロジェクトを立ち上げとアプリの設定を行います。

新規プロジェクトの立ち上げ

Welcom to Xcode画面から、Create a new Xcode projectを選択します。そしてSingle View Appを選択します。

今回は以下のように設定します。

入力項目 入力値
Product Name GestureFlash
Team None
Oganization Name 任意(”ALJ”など自分が所属している組織の名前を入れる)
Oganization Identifier jp.co.al-j
Language Swift
User Interface Storyboard
Use Core Data チェックを外す
include Unit Tests チェックを外す
include UI Tests チェックを外す

アプリの設定

新規プロジェクトを立ち上げたら、アプリの設定を行います。ここで、アプリのアイコンやアプリの表示タイトル(Bundle Name)、サポートするデバイスの向き(Device Orientation)を以下のように、設定します。

Device Orientation の設定

対象デバイスとサポートするデバイスの向きの設定を行います。

ナビゲータエリアからproject > General を選択し、Development infoを以下の設定にします。

Development info

Target Device 入力値
iOS13.5 iPhone チェックをする
iOS13.5 iPad チェックをはずす
Device Orientation Portrait チェックをする
Device Orientation Upside Down チェックをはずす
Device Orientation Landscape Left チェックをはずす
Device Orientation Landscape Right チェックをはずす

Bundle Nameの設定

アプリの表示タイトルを決めている設定項目Bundle Nameを変更します。

infoタブを選択し、Bundle Nameを次のように変更します。

Custom iOS Target Properties

Key Type Value
Bundle Name String ジェスチャー

素材取り込み

以下のリンクから素材ファイルをダウンロードします。

素材ダウンロード

今回使用する素材は以下の通りです。

ファイル名 説明
icon-20pt@2x.png Notification用x2画像
icon-20pt@3x.png Notification用x3画像
icon-60pt@2x.png アイコン用x2画像
icon-60pt@3x.png アイコン用x3画像
icon-29pt@2x.png 設定用x2画像
icon-29pt@3x.png 設定用x3画像
icon-40pt@2x.png Spotlight用x2画像
icon-40pt@3x.png Spotlight用x3画像
icon-1024pt@1x.png AppStore用アイコン画像
btn_start.pdf 「スタート」ボタン用画像
btn_go_top.pdf 「トップへ」ボタン用画像
swipe-up.pdf 「上スワイプ」画像
swipe-down.pdf 「下スワイプ」画像
swipe-left.pdf 「左スワイプ」画像
swipe-right.pdf 「右スワイプ」画像
pinch-in.pdf 「内向きピンチ」画像
pinch-out.pdf 「外向きピンチ」画像
rotate-left.pdf 「反時計回り回転」画像
rotate-right.pdf 「時計回り回転」画像

ダウンロードしたら、zipファイルを解凍してiconファイル画像ファイルを、下図のとおり設定してください。

iconファイル

画像ファイル

画面のデザイン

画面を作成していきます。

スプラッシュ画面の設定

スプラッシュ画面を作成します。

今回は以下のように設定します。

background の設定

ナビゲーターエリアからLaunchScreen.storyboard を選択し、ViewController > View > Attributes inspector を選択し、 Background を以下へ設定します。

Viewプロパティ名 設定値 Hex Color
Background Custom F39431

Labelの設置

ライブラリーエリアを選択し、 View 上に Label を配置します。

Label に対して以下の設定を行います。

Labelプロパティ名 設定値
Text ジェスチャーゲーム
Color White Color
Font 50px
Alignment centor
Lines 2

AutLayoutの設定

Labelに対して、以下の「制約(Constraint)」を設定します。

警告が表示されていなければOKです。

もし、警告が表示していた場合は、候補が表示されますので適宜対応してください。

メイン画面の設定

アプリの設定が完了したら、画面のデザインを行なっていきます。今回は3つの画面(スタート画面プレー画面結果表示画面)を持つアプリを作成します。Main.storyboardを開き、以下の手順に従って画面のデザインを行いましょう。

スタート画面のデザイン

まず、スタート画面からデザインしていきます。初期状態で、1画面分のViewは用意されているので、それをスタート画面とします。

今までの講義の画面デザインと同様にLabelButtonを使って下記の画面を作ってください。


スタート画面のAuto Layout対応

今までの講義で作ったアプリと同様にAuto Layoutを使って下記の図を参考にスタート画面のレイアウト調整をしてください。

ハイスコアラベルの設定

ハイスコア値(1位)ラベルの設定

ハイスコア(2位)ラベルの設定

ハイスコア(3位)ラベルの設定

スタートボタンの設定

説明ラベルの設定

タイトルラベルの設定

これでAuto Layoutの設定は完了です。

プレー画面のデザイン

スタート画面のデザインが完了したら、プレー画面のデザインに移ります。先ほど述べたとおり、初期状態では1画面分のViewしか含んでいません。そこで、「クイズ」の時と同様、ViewStoryboard上に新規追加します。ライブラリーエリアよりView Controllerを選び、Storyboard上に配置します。

ImageViewの配置

プレー画面では、背景を各ジェスチャーの画像とします、その画像を表示するために、正方形のImageViewを配置します。

Viewプロパティ名 設定値
Width 300
Height 300

Labelの配置

次に画面上部にクイズのいくつかのLabelを配置します。まずは、以下のとおり、静的なLabel(コードから変更を受けないLabel)を配置します。

静的なLabelの配置が終わったら、コードから変更受ける動的なLabelを配置します。今回配置するのは、経過時間を示すLabelと、こなしたジェスチャーの数を示すLabelです。

プレー画面のAuto Layout対応

プレー画面のAuto Layout設定を行います。下図を参考に設定してみてください。

「経過時間」ラベルの設定

「経過時間」の値ラベルの設定

問題総数(30)のラベル設定

問題をこなした数のラベル設定

「現在:」ラベルの設定

ジェスチャー画像の設定

下図のように表示されればOKです。

※ 警告やエラーが表示される場合

結果表示画面のデザイン

3つの画面のうち、最後の結果表示画面のデザインに移ります。プレー画面同様、結果表示画面用のViewControllerStoryboard上に新規追加します。

Labelの配置

Labelの配置します。下記のように配置してください。

1つは所要時間(記録タイム)を表示するものです。もう一つは、記録がハイスコアか否かを示すLabelで、ハイスコアを更新した場合、表示されます。

Buttonの配置

「トップへ」Buttonを配置します。

結果表示画面のAuto Layout対応

結果表示画面のAuto Layout設定を行います。下図を参考に設定してみてください。

新規View ControllerのアサインとSegueの設定

今回も前回同様、それぞれの画面に対して新規にView Controllerを作成します。また、各画面間のSegueを設定します。

新規View Controllerの作成

まずは、プレー画面用のView Controllerクラスから作成します。ナビゲーターエリアの中にある、GestureFlashと書かれたフォルダを右クリックし、前回同様Cocoa Touch classを選択し、Nextをクリックし、View Controllerを作成します。

以下の表の通り、クラス名とオプションを設定し、Nextをクリックし、既存のView Controllerとフォルダに保存します

入力項目 入力値
Class PlayViewController
Subclass of UIViewController
Also create XIB file チェックを外す
Language Swift

同様の手順で、結果表示画面用のView Controllerクラスも作成します。以下の表の通り、クラス名とオプションを設定します。

入力項目 入力値
Class ResultViewController
Subclass of UIViewController
Also create XIB file チェックを外す
Language Swift

ここまでの手順を正しく行った場合、ナビゲーターエリアに合計2つのファイルが新たに作成されたはずです。

新規ViewControllerのアサイン

ここまでの手順で、3つの画面用のView Controllerのひな形が作成されました。次に、新たに作ったPlay View ControllerResult View ControllerをそれぞれのViewにアサインしなければいけません。

クイズアプリの時と同様、Interface Builderからそれぞれの画面のView Controllerを選択します。



Storyboard Segueの設定

Storyboard Segueは、遷移元の画面と遷移先の画面を「線」で結ぶことによって設定します。今回は、以下の3つの画面遷移があるので、それぞれに対してStoryboard Segueを設定していきます。なお、Segueの種類は全てPresent Modallyを選択します。

  • スタート画面 → プレー画面
  • プレー画面 → 結果表示画面
  • 結果表示画面 → スタート画面

お気づきかもしれませんが、この流れは前回や前々回作成した手順と全く同じです。詳しい手順がわからない場合は、前回のテキストを参照してください。すべて正しく行えた場合は以下のとおりすべての画面が線で繋がります。

スタート画面からプレー画面への遷移



プレー画面から結果表示画面への遷移

ボタンに紐付いていない、Play View ControllerからResult View ControllerへのSegueに識別子をつけます。その識別子はtoResultViewとします。

結果表示画面からスタート画面への遷移

最後に、ユーザーが結果を見終わったら、スタート最後へ戻れるように、Segueを設定します。まず最初にもとに戻りたいViewControllerに以下の記述を行います。ここではViewController.swiftに記述します。

ViewController.swift

@IBAction func backView(segue: UIStoryboardSegue) {

}

次に、結果表示画面上に「トップへ」ボタンを設置したかと思いますが、このボタンに対してSegueを設定します。キーボード上の「control」キーを押しながら、ViewController上のExitというボタンにドラッグしてください。

すると先ほど作成したメソッドが表示されていると思いますのでそれを選択してください。

これで元に戻るための接続が完了しました。

これにて、全てのStoryboard Segue(画面遷移)の設定は完了となります。すべて正しく行った場合、以下のように、すべての画面がそれぞれSegueを示す矢印で結ばれているはずなので、確認してください。

UIオブジェクトをコードと結びつける

Main.storyBoardとコードをアシスタントエディタで接続しておきます。

ViewController

class ViewController: UIViewController {

//ハイスコアラベル用
@IBOutlet weak var highScore1Label: UILabel!
@IBOutlet weak var highScore2Label: UILabel!
@IBOutlet weak var highScore3Label: UILabel!

PlayViewController

class PlayViewController: UIViewController {

//タイマーラベル用
@IBOutlet weak var timeLabel: UILabel!

//正解数表示ラベル用
@IBOutlet weak var completedGesturesLabel: UILabel!

//問題画像用
@IBOutlet weak var gestureImage: UIImageView!

ResultViewController

class ResultViewController: UIViewController {

//タイマーラベル用
@IBOutlet weak var timeLabel: UILabel!

//ハイスコア更新ラベル用
@IBOutlet weak var newHighScoreLabel: UILabel!

これにて、画面デザインは完了となります。

スタート画面のコーディング

ここでは、スタート画面View Controllerのコーディングを行います。

メンバー変数の定義

スタート画面は上記のUIオブジェクトと結びつけたものでメンバー変数の宣言は終わりなので下記のようになっていればなにもしなくて大丈夫です。

ViewController.swift

class ViewController: UIViewController {

//ハイスコアラベル用
@IBOutlet weak var highScore1Label: UILabel!
@IBOutlet weak var highScore2Label: UILabel!
@IBOutlet weak var highScore3Label: UILabel!

User Defaults領域の参照

User Defaultsとは、すべてのiOSアプリが持つ、データ保存領域です。基本的に、様々データを「キー(キーワード)」と紐付けることで、簡単に保存できます。このデータはアプリが終了しても、しっかり残るので、ゲームの途中経過やユーザーの設定を保存するのに最適です。今回は、ハイスコアを記録するのに使います。

スタート画面では、ハイスコアの読み出しを行います。もしハイスコアが記録されている場合は画面上のラベルの値をそれに応じて更新します。今回User Defaultには、以下のとおり3つのデータの保存と参照をします。

キー データの種類
high_score1 ハイスコア1位の記録
high_score2 ハイスコア2位の記録
high_score3 ハイスコア3位の記録

User Defaultsの参照は非常に簡単です。基本的には、NSUserDefaultsクラスのメソッド1つで、指定したキーの値を変数に代入することができます。以下の通り「override func viewDidAppear(animated: Bool)」メソッドを追加して下さい。

ViewController.swift

// ▼▼ 追加 ▼▼

//viewが表示される度に呼ばれるメソッド
override func viewDidAppear(_ animated: Bool) {

//User Defaultsへアクセスする
let defaults = UserDefaults.standard

//1位から3位までのハイスコアを取得し、double型の変数に格納
let highScore1 = defaults.double(forKey: "highScore1")
let highScore2 = defaults.double(forKey: "highScore2")
let highScore3 = defaults.double(forKey: "highScore3")

//NSLogによるデバッグメッセージ
NSLog("ハイスコア: 1位-%f 2位-%f 3位-%f", highScore1,highScore2, highScore3)

//ハイスコアの存在を確認
//もし、ハイスコアが存在する場合(0でない場合)は画面の一覧に表示
if highScore1 != 0 {
highScore1Label.text = String(format: "%.3f 秒", highScore1)
}
if highScore2 != 0 {
highScore2Label.text = String(format: "%.3f 秒", highScore2)
}
if highScore3 != 0 {
highScore3Label.text = String(format: "%.3f 秒", highScore3)
}
}

// ▲▲ 追加 ▲▲

User Defaultsを読み出す際、今回は読み出し結果をDouble型の変数として返すdoubleForKeyメソッドを使っています。もしキーに該当する値が存在しない場合は0が返されます。この他にも、様々データ型で読みだし結果を返すメソッドが用意されているので、適宜、Appleのドキュメント等を参考にして、最も適当なものを選んで下さい。

ViewControllerのライフサイクルについて

ライフサイクルとはアプリが起動してから、バックグラウンドへ移動するまでの間に処理されるコードのサイクルの事です。

NSLogによるデバッグメッセージの表示

コーディングを行う際、正常な動作を確かめる上で、変数の状態や処理の内容を見る必要が出てきます。

例えば、今回の例で、ハイスコアのラベルの値が変更されないというバグに遭遇したとします。その場合、User Defaultsのデータ読み出しの段階で問題が起きているのか、ラベルの値を更新する処理で問題があるのかを、切り分ける必要があります。このような時、NSLogによるデバッグは非常に有効となります。

使い方は非常に簡単です。コードの中に以下の一文を加えるだけです。

//NSLogによるデバッグメッセージ
NSLog("ハイスコア: 1位-%f 2位-%f 3位-%f", highscores1,highscores2, highscores3)

この例では、ハイスコアを格納するDouble型の変数の値を出力しています。基本的な使い方はStringでの引数に「format」を指定する方法と同じです。前半で、表示する文字列の書式を指定し、後半に参照する変数を指定します。

これにて、ViewControllerの実装は完了となります。

結果表示画面のコーディング

「クイズ」の時と同様、プレー画面から結果表示画面へ遷移する際、データの受け渡しを行う都合上、次は、結果表示画面のコーディングを行なっていきます。

ResultViewControllerのメンバー変数

まずはメンバ変数から宣言していきます。以下のとおり、メンバ変数を宣言してください。

ResultViewController.swift

class ResultViewController: UIViewController {

@IBOutlet weak var timeLabel: UILabel!

@IBOutlet weak var newHighScoreLabel: UILabel!

// ▼▼ 追加 ▼▼

//プレー画面から受け渡される経過時間を格納するための変数
var time = TimeInterval()

// ▲▲ 追加 ▲▲

上記のTimeIntervalは、精密な経過時間を扱うことのできるクラスです。これを用いることによって、マイクロ秒単位で時間の計測ができます。その使い方は後ほど説明します。

ハイスコアの計算User Defaults領域への書き込み

結果表示画面では、呼び出された段階で、「time」にゲームの所要時間(記録タイム)が格納されています。
まずはその値を元に、既存のハイスコアと比較をし、上回った場合は書き換えを行うメソッドを実装します。
以下のとおり、ResultViewController.swiftにコードを記述します。

ResultViewController.swift

// ▼▼ 追加 ▼▼

func checkHighScore() {

//ハイスコアが更新されかどうかの管理(スコア比較前は「偽」に)
var newHighScore = false

//一度「ハイスコア更新」ラベルを非表示
newHighScoreLabel.isHidden = true

//User Defaultsへアクセスする
let defaults = UserDefaults.standard

//1位から3位までのハイスコアを取得し、double型の変数に格納
var highScore1 = defaults.double(forKey: "highScore1")
var highScore2 = defaults.double(forKey: "highScore2")
var highScore3 = defaults.double(forKey: "highScore3")

//(全てのハイスコアが既にある場合)比較の結果、今回のtimeが当てはまる順位に記録を挿入
//1位より早い場合
if highScore1 != 0 && time <= highScore1 {
highScore3 = highScore2
highScore2 = highScore1
highScore1 = time
newHighScore = true

//2位より早い場合
} else if highScore2 != 0 && time <= highScore2 {
highScore3 = highScore2
highScore2 = time
newHighScore = true

//3位より早い場合
} else if highScore3 != 0 && time <= highScore3 {
highScore3 = time
newHighScore = true
}
//ハイスコアがまだ格納されていない場合のtimeとの比較
//1位がまだない場合
else if highScore1 == 0 {
highScore1 = time
newHighScore = true
//2位がまだなく、1位より遅い場合
} else if highScore2 == 0 && time >= highScore1 {
highScore2 = time
newHighScore = true
//3位がまだなく、2位より遅い場合
} else if highScore3 == 0 && time >= highScore2 {
highScore3 = time
newHighScore = true
}

//新しいハイスコアをUser Defaultsに保存
defaults.set(highScore1, forKey: "highScore1")
defaults.set(highScore2, forKey: "highScore2")
defaults.set(highScore3, forKey: "highScore3")

//もし、ハイスコアが更新された場合は「ハイスコア更新」ラベルを表示
if newHighScore == true {
newHighScoreLabel.isHidden = false
}
}

// ▲▲ 追加 ▲▲

ここでは、User Defaultsの値の読み出しを行い、それらを今回の記録である「time」と比較しています。なお、「time」はNSTimeIntervalのインスタンスですが、Double型の変数としても扱えます。比較を行ったあと、最新のハイスコアをUser Defaultsに書き込みます。なお、「ハイスコア更新」ラベルですが、比較前で非表示とします。比較後に、もしハイスコアが更新されるようなことがあれば、表示状態にします。

ResultViewControllerの初期処理

ハイスコアの更新を行うメソッドの実装が終わったら、初期処理を実装します。以下のとおり、ResultViewController.swiftの「viewDidLoad」を実装します。

ResultViewController.swift

override func viewDidLoad() {
super.viewDidLoad()

// ▼▼ 追加 ▼▼

//タイムを表示
timeLabel.text = String(format: "%.3f 秒", time)
//ハイスコアの判定
self.checkHighScore()

// ▲▲ 追加 ▲▲
}

ここでは、timeの値をラベルに反映し、先程実装したハイスコアの更新処理を呼び出しています。

プレー画面のコーディング

次に、本アプリの中心となるプレー画面のコーディングを行なっていきます。

タッチジェスチャーとは?

まず、タッチジェスチャーに関する説明を行なっていきます。昨今のスマートフォンやタブレットは基本的にタッチパネルで操作しますが、これらはページをめくる動作やスクロール動作などを直感的にタッチで行えます。これらをタッチジェスチャーといいます。iOSデバイスでは一般的なスワイプ(1本指でスクロール)をはじめ、回転(2本指で画面上の写真を回転)、ピンチ(拡大・縮小)などのジェスチャーを非常に簡単に検知できるようになっています。

今回はこの中でも、Swipe・Pinch・Rotateの3タイプに着目します。これら3タイプのジェスチャーを30個分、ランダムにユーザーに提示し、それをすべてやり終えるまでの時間を竸います。

メンバー変数の宣言

まずは、メンバー変数の宣言から行なっていきます。
以下のとおり、PlayViewController.swiftを編集して下さい。

PlayViewController.swift

class PlayViewController: UIViewController {

@IBOutlet weak var timeLabel: UILabel!
@IBOutlet weak var completedGesturesLabel: UILabel!
@IBOutlet weak var gestureImage: UIImageView!

// ▼▼ 追加 ▼▼

//ゲームの経過時間を計測
var startTime = NSDate()

//こなしたジェスチャーの数を管理
var completedGestures = Int()

//現在の問題で、発見すべきジェスチャーを記録
var currentGesture = Int()

//経過時間を画面に表示するためのタイマー
var timer = Timer()
var timerCount = Double()

// ▲▲ 追加 ▲▲

ここにある、NSDateは、時間を取り扱うためのクラスです。その扱いかたは、後ほどのNSTimeIntervalと併せて解説します。また、NSTimerはタイマーによる割り込み処理を行うためのクラスです。これにより、指定した時間間隔毎に、任意のメソッドを呼び出すことができます。

経過時間の計測とResult View Controllerへの遷移

まずは、時間の経過時間の測定に関する説明をします。その前に、以下のとおり、PlayViewController.swiftを編集して下さい。

PlayViewController.swift

// ▼▼ 追加 ▼▼

//結果表示画面へのSegueの発動
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {

//所要時間を計測
var elapsedTime = startTime.timeIntervalSinceNow
elapsedTime = -(elapsedTime)

//ResultViewController(RVC)のインスタンスを作成し、
//RVCクラスのメンバー変数である「time」に値を渡す
if segue.identifier == "toResultView" {
let rvc = segue.destination as! ResultViewController
rvc.time = elapsedTime
}
}

// ▲▲ 追加 ▲▲

これは、「クイズ」でも取り扱った、Segueを発動させるメソッドです。ゲームの経過時間(記録タイム)を測定する場合、ゲームが開始された時間から、完了までの時間を計測することになります。そこで今回は、ゲーム完了時にSegueを発動させるようにし、そのタイミングで所要時間を計測します。

経過時間を得るためには以下のように、NSTimeIntervalとNSDateのインスタンスを組み合わせます。

//所要時間を計測
var elapsedTime = startTime.timeIntervalSinceNow

ここにある「startTime」は後ほど、初期処理を実装する際に、セットします。NSDateクラスのインスタンスに対して、「timeIntervalSinceNow」を呼び出すと、現在までの経過時間をマイクロ秒単位で得ること事ができます。その際、「開始時刻」から「現在時刻」が差し引かれるので、経過時間は負の数となります。そこで、実経過時間を得るためには、以下のようにする必要があります。

elapsedTime = -(elapsedTime)

経過時間を取得した後は、それをResult View Controllerに渡します。その手法は「クイズ」の時と全く同じです。

次のジェスチャーを提示するメソッド

今回は、ランダムに提示された30個のジェスチャーをこなすことがゲームの目的となります。そこで、指定された1つのジェスチャーが正しく検知された場合、次のジェスチャーを提示するメソッドを実装していきます。以下に示すように、PlayViewController.swiftを編集して下さい。

PlayViewController.swift

//次の問題を表示
func nextProblem() {

//もし出題規定数(ジェスチャー30個)に達している場合
if completedGestures == 30 {
//結果表示画面へのSegueを始動
self.performSegue(withIdentifier: "toResultView", sender: self)
} else {

//配列にジェスチャーを示す画像取り込み
let gestureIcons = [
UIImage(named: "swipe-right"),
UIImage(named: "swipe-left"),
UIImage(named: "swipe-up"),
UIImage(named: "swipe-down"),
UIImage(named: "pinch-in"),
UIImage(named: "pinch-out"),
UIImage(named: "rotate-right"),
UIImage(named: "rotate-left")
]

//乱数をもとに、次のジェスチャーを選択
currentGesture = Int(arc4random() % 8)
NSLog("got new gesture current: %d", currentGesture)

//画面に出てるジェスチャーの画像を差し替え、問題番号を更新
gestureImage.image = gestureIcons[currentGesture]
completedGesturesLabel.text = String(format: "%d", completedGestures)
}
}

基本的に、このメソッドでは、これまでにこなしたジェスチャーの数をもとに、次の画面へのSegueを発動するか、次の問題を提示するかを判断します。次の問題を提示するとなった場合、ランダムにジェスチャーが指定されます。

今回は全部で3タイプ・合計8種類のジェスチャーを扱うものとし、それぞれに識別番号を指定します。ます。それらを以下に示します。

種類 識別番号 画像
右スワイプ 0 swipe-right.pdf
左スワイプ 1 swipe-left.pdf
上スワイプ 2 swipe-up.pdf
下スワイプ 3 swipe-down.pdf
内向きピンチ 4 pinch-in.pdf
外向きピンチ 5 pinch-out.pdf
時計回り回転 6 rotate-right.pdf
反時計回り回転 7 rotate-left.pdf

これらをランダムに指定し、識別番号を「currentGesture」に代入します。その後、画面に出ているラベルやImage Viewを適宜更新します。

ジェスチャーの認識

今回は、ジェスチャーの認識を行うのですが、そのための手段としてiOS SDKでは、UIGestureRecognizerというものを提供しています。これを利用することによって、簡単にジェスチャーを認識することができます。

プロトコルの設定

それでは、まずその準備からはじめます。以下の通り、PlayViewController.swiftにプロトコルの設定を施します。

PlayViewController.swift

【変更前】

class PlayViewController: UIViewController {

【修正後】

class PlayViewController: UIViewController, UIGestureRecognizerDelegate {

UIGestureRecognizerのセット

プロトコルの設定が終わったら、UIGestureRecognizerのセットを行います。以下のとおり、PlayViewController.swiftを編集して下さい。

PlayViewController.swift

// ▼▼ 追加 ▼▼

func setGestureRecognizers(){
//pinch
let pinchRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(PlayViewController.pinchDetected(sender:)))
self.view.addGestureRecognizer(pinchRecognizer)

//rotate
let rotationRecognizer = UIRotationGestureRecognizer(target: self, action: #selector(PlayViewController.rotationDetected(sender:)))
self.view.addGestureRecognizer(rotationRecognizer)

//swipe-right
let swipeRightRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(PlayViewController.swipeRightDetected(sender:)))
swipeRightRecognizer.direction = UISwipeGestureRecognizer.Direction.right
self.view.addGestureRecognizer(swipeRightRecognizer)

//swipe-left
let swipeLeftRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(PlayViewController.swipeLeftDetected(sender:)))
swipeLeftRecognizer.direction = UISwipeGestureRecognizer.Direction.left
self.view.addGestureRecognizer(swipeLeftRecognizer)

//swipe-up
let swipeUpRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(PlayViewController.swipeUpDetected(sender:)))
swipeUpRecognizer.direction = UISwipeGestureRecognizer.Direction.up
self.view.addGestureRecognizer(swipeUpRecognizer)

//swipe-down
let swipeDownRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(PlayViewController.swipeDownDetected(sender:)))
swipeDownRecognizer.direction = UISwipeGestureRecognizer.Direction.down
self.view.addGestureRecognizer(swipeDownRecognizer)
}

// ▲▲ 追加 ▲▲

ここにあるコードは非常に長く、暗号文のように思うかもしれませんが、これら一連のコードのよって、それぞれのジェスチャーを認識する準備が整います。

スワイプの認識

スワイプは前述したとおり、1本の指で上下左右になぞるジェスチャーです。これらは方向ごとにそれぞれ独立したUIGestureRecognizerをセットします。
UIGestureRecognizerがセットされた後、iOSデバイスがジェスチャーを認識し場合、次のメソッドが呼ばれます。PlayViewController.swiftに追記して下さい。

PlayViewController.swift

// ▼▼ 追加 ▼▼

//スワイプ(右)
@objc func swipeRightDetected(sender: UIGestureRecognizer){
print("スワイプ(右)")
if currentGesture == 0 {
completedGestures += 1
self.nextProblem()
}
}

//スワイプ(左)
@objc func swipeLeftDetected(sender: UIGestureRecognizer){
print("スワイプ(左)")
if currentGesture == 1 {
completedGestures += 1
self.nextProblem()
}
}

//スワイプ(上)
@objc func swipeUpDetected(sender: UIGestureRecognizer){
print("スワイプ(上)")
if currentGesture == 2 {
completedGestures += 1
self.nextProblem()
}
}

//スワイプ(下)
@objc func swipeDownDetected(sender: UIGestureRecognizer){
print("スワイプ(下)")
if currentGesture == 3 {
completedGestures += 1
self.nextProblem()
}
}

// ▲▲ 追加 ▲▲

これらは、上下左右それぞれの方向のスワイプを検知した場合に呼ばれます。その際、「currentGesture」のがチェックされ、指定されたジェスチャーと検知したジェスチャーが一致した場合は、次のジェスチャーを提示する「nextProblem」が呼ばれます。

回転の認識

回転は、2本の指で画面上を回転するジェスチャーを指します。そのジェスチャーを検知した場合に呼ばれるメソッドを、以下のとおり記述して下さい。

PlayViewController.swift

// ▼▼ 追加 ▼▼

//回転
@objc func rotationDetected(sender: UIRotationGestureRecognizer){
print("回転")
let radians = sender.rotation
let degrees = radians * CGFloat(180 / Double.pi)

if degrees > 90 {
if currentGesture == 6 {
completedGestures += 1
self.nextProblem()
}
}else if degrees < -90 {
if currentGesture == 7 {
completedGestures += 1
self.nextProblem()
}
}
}

// ▲▲ 追加 ▲▲

この時、最初に2本の指が画面に置かれた場所を基準とし、少しでも回転を検知した場合、回転した相対量が「ラジアン」の数値として返されます。まず、「ラジアン」を「度」に変換する必要があります。その際、以下の計算式を用います。

let degrees = radians * CGFloat(180/M_PI)

相対的な回転量が90度を超えた場合、ジェスチャーがこなされたというようにします。この時、スワイプ同様「currentGesture」の値と比較され、提示されたジェスチャーと認識したジェスチャーが一致した場合、「nextProblem」が呼ばれます。

なお、開始位置から右(時計回り)方向に回転された場合、正の値が返されます。開始位置から左(反時計回り)方向に回転された場合、負の値が返されます。

ピンチの認識

次に、ピンチの認識を行います。画面上で2本の指を近づけたり遠ざけたりするジェスチャーです。そのジェスチャーを検知した場合に呼ばれるメソッドを、以下のとおり記述して下さい。

PlayViewController.swift

// ▼▼ 追加 ▼▼

//ピンチ
@objc func pinchDetected(sender: UIPinchGestureRecognizer){
print("ピンチ")
let scale = sender.scale

if scale > 2.4 {
if currentGesture == 5 {
completedGestures += 1
self.nextProblem()
}
}else if scale < 0.4 {
if currentGesture == 4 {
completedGestures += 1
self.nextProblem()
}
}
}

// ▲▲ 追加 ▲▲

この時、最初に2本の指が画面に置かれた時の、2本の指の距離が「1」となります。同じ直線上で少しでも距離が変化した場合、変化の相対量が数値が返されます。例えば、「1以上の数値」が返された場合は、2本の指が離れていることになります。一方、「1未満の数値」が返された場合は、2本の指が近づいていることになります。

今回は、相対距離が「2.4」を上回った場合、外向きのピンチがこなされたとします。同様に、相対距離が「0.4」を下回った場合、内向きのピンチがこなされたとします。ピンチが正常にこなされたと認識できた場合、「currentGesture」の値と比較され、正解の場合は「nextProblem」が呼ばれます。

これにて、ジェスチャーの検知に関するコードの記述は完成となります。

Play View Controllerの初期処理

次に、Play View Controllerの初期処理を行います。以下の通り「viewDidLoad」を編集します。

override func viewDidLoad() {
super.viewDidLoad()

// ▼▼ 追加 ▼▼

//こなしたジェスチャーの数を0にリセット
completedGestures = 0

//Gesture Recognizersをセット
self.setGestureRecognizers()

//最初の問題を表示
self.nextProblem()

// ▲▲ 追加 ▲▲
}

ここで着目すべきは、「startTime」の値が現在の時刻にセットされているところです。冒頭で説明したように、これにより、30個のジェスチャーをこなすまでの所要時間(タイム記録)を測定することができます。

NSTimerによるタイマー割り込み処理

次に、よく使う機能としてNSTimerがあります。このNSTimerはタイマー割り込み処理を提供するクラスです。NSTimerが発動されると、指定された間隔毎割り込みが発生し、指定されたメソッド呼ばれます。今回はこれを用いて、ゲーム画面中に経過時間の概算値を表示する機能を実装します。まず、PlayViewController.swiftの「viewDidLoad」を以下の通り編集します。

PlayViewController.swift

override func viewDidLoad() {
:
(省略)
:
// ▼▼ 追加 ▼▼

//経過時間を表示するタイマーの始動
//0.1秒毎に「-(void)onTimer」が呼ばれる
timerCount = 0
timer = Timer.scheduledTimer(
timeInterval: 0.1,
target: self,
selector: #selector(PlayViewController.onTimer(timer:)),
userInfo: nil,
repeats: true
)

// ▲▲ 追加 ▲▲
}

さらに、0.1秒毎に呼ばれる「onTimer」というメソッドを実装します。

PlayViewController.swift

// ▼▼ 追加 ▼▼
//0.1秒毎に呼ばれる経過時間表示を更新処理
@objc func onTimer(timer: Timer) {
timerCount = timerCount + 0.1
timeLabel.text = String(format: "%.1f", timerCount)
}
// ▲▲ 追加 ▲▲

NSTimerが発動されると、指定時間間隔(今回は0.1秒)毎に「onTimer」が呼ばれます。そして、「onTimer」内で経過時間を示すラベルの値に「0.1」を足します。これにより、ユーザーはゲーム中に概算経過時間を知ることができます。

ビルドと動作試験

これにて、すべての作業は完了となります。編集内容を全て保存し、ビルドを行なってください。このテキストの内容をすべて正しくやった場合、特に問題なくアプリが動作するはずです。

ここで、一点注意があります。iOSシミュレーター上では、マウスを使う都合上、ジェスチャー入力に限界があります。ジェスチャーの動作確認を行う際は、実機での検証を強く勧めます。

プレー画面で、3タイプ・8種類のジェスチャーがしっかり認識できること、NSTimerによって、概算経過時間が表示されることを確認します。また、結果表示画面では記録タイムが表示されること、また、スタート画面上でハイスコアが正常に表示されることも確かめて下さい。

デバッグメッセージの確認

「ジェスチャーゲーム」では、各所にNSLogを用いたデバッグメッセージを表示する命令を記述しています。ここでは、このNSLogによるデバッグメッセージの確認方法を解説します。
デバッグメッセージは、以下のように、デバッガーエリアに表示されます。

このNSLogによるデバッグメッセージは非常に便利かつ、よく使う機能なので、覚えるようにして下さい。

まとめ

今回はiOS SDKにおけるタイマーによる割り込み処理や時間計測の手法を学びました。また、ジェスチャー入力の検知手法、および、NSLogを用いたデバッグメッセージの出力方法を扱いました。

今回扱ったタイマーによる割り込み処理やジェスチャー入力は、様々なアプリで応用されています。よく使う機能なので、ぜひ使い方をマスターして下さい。また、自分流にゲームをアレンジして、いろんな機能を付加して下さい。