リブートキャンプ by Swift 目次
前回 のサンプルの説明。
↓これね
サンプル:
http://tetera.jp/xcc/book-sample/RPG-2.zip
ちなみに、このサンプルはGitを使って、この1つ前のサンプルからの変化を比較できるようにしてます。
↓これね
サンプル:
http://tetera.jp/xcc/book-sample/RPG.zip
なので、ワークスペースウィンドウのナビゲーションエリアでViewController.swiftを選んでタイトルバーにあるShow the Version Editorボタンをクリックすると、ViewController.swiftプログラムコードの比較画面になるっす。
画面狭っ!と思う人はユーティリティエリアを隠しましょう。
まさかとは思うが、ナビゲーションエリアが見当たらない人は、Hide or Show Navigatiorボタンをクリックだ!
この比較画面を使うと履歴(任意のタイミングで記録させた、プログラムコード)を左右で比較できて便利っす。
履歴を残すには、Source Control→Commit…メニューを使います。興味のある人は「 Source Control Xcode 」あたりで検索して見てちょ。 「 iPhoneアプリ開発、その(239) ご〜まぁり・さん 」でも軽く触れてる。あとGitHubについても「 iPhoneアプリ開発、号外 GitHubを使おうぜい 」で軽く紹介。
元の状態に戻すにはShow the Standard Editorボタンをクリックだ。
ここらへんの画面操作は、Viewメニューにかたまってるので、わからない時はViewメニューを探してみましょう。
ちなみに前回の変更で、プロジェクトに新たに追加されたファイルには、ナビゲーションエリアでA(added:追加された)てのが付いてます。
変更したものはM(modified:変更された)が付く。
でまあ、前回の変更についてなんですが…
script配列の要素を文字列から辞書に変更する点は説明した通りっす。
配列がわからん人は「色々と脱線 」を読みましょう。辞書がわからん人は「アンラップしてチン♪ 」だ。
class ViewController: UIViewController {
・・・
let script : [ [String:Any] ] = [
・・・
あとプロパティにbackgroundImageView てのを加えたのは、画面タップ時のアクションに背景を消したり見せたりてのを加えたため。
じゃ、その下のbgmPlayer は何かっていうと、調べた人もいると思うけど音楽プレーヤオブジェクトです。
class ViewController: UIViewController {
・・・
var scriptIndex = 0 // 現在の場面の段階
var backgroundImageView :UIImageView! // 背景
var bgmPlayer :AVAudioPlayer?
override func viewDidLoad() {
音楽プレーヤオブジェクトの作り方、扱い方は後で説明するとして、先にtapメソッドから呼び出してるexecute からやっちゃいましょう。
tapメソッドでやってること自体は、scriptIndexで指定されるスクリプト要素を取り出してexecuteに渡して、scriptIndexの値を1つ進めてるだけなんで、時に説明はいらんよね。
func tap() {
if scriptIndex < script.count {
execute (action:script[scriptIndex])
scriptIndex += 1
}
}
// action:で渡された辞書に従ってセリフや状況を更新する。
func execute (action: [String : Any]) {
・・・
}
で、executeメソッドの話。
コメントに書いてるようにaction:で渡された辞書に従ってセリフや状況を更新してます。
まず、最初にやってるのが渡された辞書が、前回決めた単独の指示なのか、それとも指示を集めたグループかの判断。
指示を集めたグループってのは、一回の作業で複数の作業を指示できるように考えたもので、"グループ"というキーワードとその要素として[ [String:Any] ]を持たせるようにしてます。
脚本の中に、脚本を埋め込んだ状態です。
ちなみに、今回のscript配列のような入れ子構造を、一般に再帰的な構造って言います。UIViewが子UIViewを持って、その子がまた子供を持つて構造なんかもそう。
で、"グループ"の方の脚本は、タップで実行される1つのアクションとなるようにしてる。
で、これを実現するためにやってるのがexecute初頭のif文なわけですよ。
辞書の要素を実行するのはやっぱりexecute なんで、execute を呼び出し点に注目。こういうのを再帰呼び出しって言います。
func execute (action: [String : Any]) {
// "グループ"キーワードを持つなら、複数の処理を配列で持っている
if let actions = action["グループ"] as? [ [String : Any] ] {
// 配列内のすべての要素を実行
for action in actions {
execute (action:action)
}
return
}
「ダイナミックなレイアウト 」で、引数やメソッド内で宣言された変数や定数は、外部で定義された同名の引数、変数、定数とは別物の記憶容器になるって説明したと思うけど、これが、このexecuteメソッドの再帰呼び出しの場合にも適用されるんですな。
そういうわけで、executeメソッドのaction:引数による動作は、そのあとの記述てことになる。
やってることは"主体"、"行動"、"対象"の値の取り出し。
"主体"、"行動"については取り出せなければ行動不能なのでメソッドから戻ってる。
guard letの意味やas? Stringの意味がわからん人は「アンラップしてチン♪ 」を読みましょう。
"対象"に関してはあってもなくてもいいのでそのまま定数targetに設定。つまりtargetはnilの場合もあるのでオプショナル型になる。
func execute(action: [String : Any]) {
if let actions = action["グループ"] as? [ [String : Any] ] {
・・・
return
}
// "主体"、"行動"、"対象"要素を取り出す。"対象"以外は必須。
guard let subject = action["主体"] as? String,
let operation = action["行動"] as? String else {
// 作業継続不可能なので戻る
return
}
let target = action["対象"] as? String
・・・
}
で、この取り出した3つの値を引数として"主体"別に処理を実行させてるのが以下のところ。
if文で振り分けてもいいんだけど、紹介がてらswitch 文使いますた。
func execute(action: [String : Any]) {
・・・
let target = action["対象"] as? String
// "主体"別に処理を実行
switch subject {
case "主人公", "須瀬":
person(subject:subject, operation:operation, target:target)
case "前景" ,"背景":
view(subject:subject, operation:operation, target:target)
case "BGM":
bgm(subject:subject, operation:operation, target:target)
default:break
}
}
switch文
switch文はif文の親戚みたいなもんで、キーワードswitchに続く値が、キーワードcaseと : (コロン)の間に ,(カンマ)で区切られて書かれている値と一致するなら、次のcaseまでの間に書かれている処理だけを実行するというもの。
caseはswitchの後ろの { ・・・ } 内に複数書ける。
switch文では、switchに続く値が取り得る値を、caseで全て羅列しないといけないんですが、今回使ってるsubjectは文字列なんで、取り得る値なんて無限なんですよ。
文字ならなんでもいい。"笹"でも、"パンダ"でも、"うっ生まれる"でもいい。
こういう場合「caseであげてるもの以外は」っていう意味で、default:というものを用意し処理を書きます。
で、やる処理なんて無いってんなら、breakを書く。
breakは「処理を終わらせてswitch文から抜けちゃいな」という命令。
こんな風に
default:
break
と書いても
default:break
と書いてもいいです。
ちなみにbreak命令は、switch文以外にwhile文やfor文でも使えます。その場合はループをやめるってことになる。
以上がswitch文の大体の説明。他に下のcase処理になだれ込めってfall throughなんてキーワードもあるけど、詳しく知りたい人は「swift switch 」で検索して調べてみてください。
switch文で選り分けられて呼び出される
func person(subject:String, operation:String, target:String?)
func view(subject:String, operation:String, target:String?)
に関しては、特に説明はいらんよね。
というわけで、残りの
func bgm(subject:String, operation:String, target:String?)
でやってるのがBGMの再生と停止。
最初に言ったようにbgmPlayerは音楽プレーヤオブジェクト。stopメッセージで再生中の音楽を停止します。作成時にnilになる場合があるので?を付けてます。
bgmPlayer?.stop()
再生の場合は、まずは、再生する曲ファイルの置き場所を見つけ、それをURLオブジェクトとして表現する必要がある。
プロジェクトのナビゲーションエリアで A がついてた項目が、この曲ファイルっす。
追加方法はドラッグ&ドロップでOK。
追加情報の画面で「Copy items if needed」にチェックつけとけば、自分のプロジェクトフォルダ内にコピーしてくれます。
ファイルの位置は、基本、プロジェクトフォルダの位置からの相対で覚えるので、プロジェクトフォルダ外にファイルを置いても問題はない。ここら辺は自分の都合で、好きな方ってことになります。
すでにプロジェクトフォルダ内に置いてるとか、プロジェクトフォルダ外に置いたままにしたいなら、チェックをつける必要はなし。
大量の画像や音楽ファイル群をフォルダをまとめて、そのフォルダをドラッグ&ドロップしてまとめて追加ってこともできる。
その場合、「Copy items if needed」下の「Added folders」の選択によって、アプリのプログラムを変える必要があるんで要注意。どう変わるかはこのあと説明。
その下の「Add to targets」は、プロジェクト内のどのターゲットに、このファイルを加えるかの指定。
ターゲットてのは、プロジェクト内のファイル群(swiftのプログラムも含め)を使って、どんなプログラムを作り出すかを定義した設計図。
いや、それがプロジェクトじゃんって突っ込まれそうですが、まあ、サブプロジェクトって解釈で大体あってるんじゃないかと。
今回ならプロジェクトには、iPhone用のアプリを作るターゲットが1つ用意されてる状態なんだけど、同じファイル群をうまく組み合わせてMac用のアプリを作ることだってできるんですよ。
その場合、Mac用のアプリを作るためのターゲットを、プロジェクトに追加します。
Xcodeは、1つのプロジェクトに複数のターゲットが持てるようになってるんですな。
そういった場合に、「Add to targets」に複数のターゲットが現れることになる。iPhone専用の画像なら、Macで使うことないんでアプリに加えても無駄になる。そこらへんを選択できるようになってるのがこれ。
とにかく、このドラッグ&ドロップで、音楽ファイルがプロジェクトに登録されたことになるんですが、このファイルはアプリ実行時はどこに置かれてるのか?
ここが、さっきの「Added folders」の選択が絡んでくる話です。
通常「Create groups」を選んだ場合、ドロップされたフォルダ内のファイルは全て、アプリごとに用意される、リソースフォルダと呼ばれるフォルダ内直下に置かれることになります。
この仕組みは、ちょっと気をつけないことがあって、例えばドロップするフォルダ内にA、B、2つのサブフォルダを用意して、それぞれに同名のファイル(traffic.mp3)を置いていた場合
アプリ実行時にリソースフォルダ直下に同名(traffic.mp3)のファイルが2つ置かれることになるんですが、その場合ファイルは後から置かれるの方に置き換えられちゃいます。
で、それはまずいんで、ドロップしたフォルダは、リソースフォルダ直下にフォルダごと置くようにしてくださいって指定できるようになってて、それが「Added folders」の「Create folder references」側の選択なわけですよ。
これはアプリ実行時の話で、プロジェクトフォルダ内にはどちらの指定でも、ドロップしたフォルダのままで置かれる。
で、このアプリ実行時のリソースフォルダの場所を調べるのに使うのがBundleクラスってことになります。
Bundle
Bundleはファイルの束(バンドル)を管理するクラスで、代表的なものがアプリ実行時のリソース群ってことになります。
こいつはBundleクラスオブジェクトのmainプロパティに設定されているBundleオブジェクトが管理してて、そのオブジェクトにurl メッセージを送ることで、リソースフォルダ直下に置かれたファイルのURL構造体を受け取ることができるんですな。
let url = Bundle.main.url(forResource: ファイル名, withExtension: 拡張子名)
URL構造体てのは、ウエブブラウザでサイトの指定に使う文字列(URL)を表現した構造体です。
↓これね
https://ameblo.jp/xcc/
URL文字列は、ファイルの位置も記述できるようになってて、その場合、最初の部分がfile:ってなって、あとはフォルダの階層をフォルダ名を / (スラッシュ)で結んでファイル名までを書いたりします。
↓こんな感じ
file:///Users/kunii/Desktop/traffic.mp3
例えば、リソースフォルダ直下に置かれたファイルの場所を知りたい場合、ファイル名がtraffic.mp3なら、urlメッセージを
url(forResource: "traffic", withExtension: "mp3")
とすれば、traffic.mp3ファイルの場所がURL構造体として戻されるわけです。
forResource:ってラベルがリソースフォルダ直下のって指定になる。
で、あとはファイル名を指定すればいいってわけで、こんな風にファイル名を拡張子とそれ以外に分離して、withExtension:引数で拡張子を指定することになってる。
拡張子ってのはファイル名の一部を指定する用語で、ファイル名が . (ドット)で区切られている場合、区切られた最後の文字列が拡張子というのが取り決めです。
今回ならmp3 が拡張子。
traffic.mp3
ただし、traffic.mp3をわざわざtrafficとmp3に分解せずに、そのままforResource:側にtraffic.mp3と指定して、withExtension:側はnilを指定してもOKっす。
url(forResource: "traffic.mp3", withExtension: nil)
私は、今回soundName 定数にファイル名を取り出して、これをそのままforResource:に指定してます。
if operation == "プレイ" {
// 再生 target指定された音ファイルを再生する
guard let soundName = target else {
return
}
guard let url = Bundle.main.url(forResource: soundName ,
withExtension: nil) else {
return
}
・・・
リソースフォルダ直下に指定のファイルがなければnilになるので、その場合はguardで終わらせてる。
これでファイルの場所が手に入ったので、これをAVAudioPlayerオブジェクト作成時の引数contentsOf:に渡してます。
で、ここでまた、見慣れない try? てのが出てくるわけですが…
bgmPlayer = try? AVAudioPlayer(contentsOf: url)
こいつは簡易の例外対応処理ってやつです。
例外
例外て何かというと、プログラムの設計上想定しない状態のことで、例えば今回のAVAudioPlayerを作るときにcontentsOf:で渡されたURLには音楽ファイルがないとダメなんだけど、こいつが画像ファイルでしたとか、ファイル自体ありませんでした〜って場合です。この状態を例外といいます。
こういう状態になったメソッドや関数の中には、例外を無視してできることを続けたり、その場でreturnしたりするものと、例外が発生しましたと知らせるものがあるんですな。後者を例外を投げるメソッドとか関数と呼びます。
ちなみに、オブジェクトを作る部分は、コンストラクタと呼ばれ、クラスごとにinitという名前の特別なメソッドで初期化されます。
例外を投げるか投げないかは作る人次第なんだけど、使う方は例外を投げるものに対しては、次のようにdo catch 文とtry キーワードを使って、投げられた例外を受け取る準備できてますよと意思表示しないとコンパイラにエラーと注意されます。
do {
・・・
try 例外を引き起こす可能性ある関数やメッセージ(メソッド)
・・・
} catch {
例外発生時の対応処理
}
例えば、0で割ろうとしたら例外を投げるdivっていう関数があったとしたら、div関数を使う側は、次のように書かないとコンパイラに怒られます。
この場合、例外はdiv(a:1, b:0)の時に投げられるので、コンソールには、最初のdiv(a:1, b:1)の結果と例外を受け取ったメッセージが
1
例外発生
てな感じで出力されるんですな。
また、投げられた例外情報は、発生した例外についての詳しい説明を持ってる場合もあって、もしそういう情報が欲しいならcatch部分を少し変更して、例外情報を受けるようにもできる。
} catch err { ← 例外情報オブジェクトをerrに受け取る
例外発生時の対応処理(err を参照したりする)
}
それとか、例外処理したくなくて、自分を呼び出した側に押し付けたいなら、自分自身を例外を投げる関数・メソッドですよと宣言(throwsキーワードをつける)してdo catch 文を書かないって手も使えます。
func 例外を丸投げする関数もしくはメソッド() throws {
do { ←do catch文で囲まない
・・・
try 例外を引き起こす可能性ある関数やメッセージ(メソッド)
・・・
} catch {
}
}
なんすか、それ、例外便利そう。と、自分でも独自の例外投げる関数やメソッドを作ってみたい人はenum使った例外情報の定義から始めるといいでしょう。
enum
enum(私はイーナムとか呼んでます)は列挙と呼ばれる種類の型で、クラスや構造体のように独自の型を定義できるようになってます。
例えば、何かの試験の結果が
合格
失格
保留
の3種類だったとして、この3つの状態を整数に対応させ
合格:0
失格:1
保留:2
ということにして、次のようにInt型の変数を使って、扱ってもいいわけだけど
let result = exam()
if result == 0 {
print("合格")
} else if result == 1 {
print("失格")
} else if result == 2 {
print("保留")
}
こういう時には、列挙型使って、次のような値を持つ型を定義した方が、プログラム読んでる人は理解しやすくなるわけですよ。
合格:OK
失格:NG
保留:Defer
具体的にはenum とcase 使って次のように書く。
enum Result {
case OK
case Defer
case NG
}
これで、クラスや構造体の時と同じく、Resultという新しい型が誕生したことになり、プログラムで使えるわけです。
let result = exam() ←exam()はResult型を戻すように再定義
if result == Result.OK {
print("合格")
} else if result == Result.NG {
print("失格")
} else if result == Result.Defer {
print("保留")
}
省略形も使えて
if result == .OK {
・・・
} else if result == .NG {
・・・
} else if result == .Defer {
・・・
}
て書いてもいい(0、1、2と書くより、とってもわかりやすい)。
で、このenum定義時にErrorプロトコルを採用しておけば、この値を例外情報として投げることができるようになります。
enum DivError : Error {
case zeroDiv
}
func div(a:Int, b:Int) throws -> Int {
if b == 0 {
throw DivError.zeroDiv
}
return a / b
}
プロトコルを採用がわからん人は「重箱の隅をつつくようにネチネチと進めてみる 」を読みましょう。
サンプル:
http://tetera.jp/xcc/book-sample/throw-catch.zip
もちろん、既存の定義済み例外情報を投げるってのでもいいっす。Errorプロトコルを採用しておけばいいので構造体やオブジェクトを投げることも可能。
例外の詳しい仕組みが知りたい人は「swift 例外処理 」あたりで検索してみてください。
で、今回の try? てのは、こいつの簡易版でして…
「例外が投げられた時はbgmPlayerにnilを設定して、そのまま処理を続けてくれちゃってくれればいいです」
という意思表示です。
bgmPlayer = try? ・・・ ←例外発生時はnilが設定される
というわけで、例外発生時は、先にも言ったようにbgmPlayerがnilになる可能性があるんで、後の処理ではbgmPlayer? とやってnilでない場合だけメッセージを送るようにしてます。
bgmPlayer? .play()
try catch文にして「・・・音が再生できません」っとユーザーに知らせるのも選択肢てはありマスタング。めんどくさいのでしてません。
曲を止めるのは
bgmPlayer?.stop()
ね。
以上、サンプルの説明は終わり。
長っがいよっっ
まあ、でも、もうすぐ必要最低限の情報は提供し終わりそうだし…
「ラノベ本」でiPhoneアプリの開発してみようかと思い立った人への義理も何とか果たせそうな気がしてきた。
次回は「帰ろっかな」で場所移動用のマップ画面が出るようにしてみる。
ではでは。