筆者:村田
こんにちは。
お盆休みいかがお過ごしでしょうか。私は暑さ凌ぎも兼ねて地元福生市の図書館に通っています。図書館はエアコンが強すぎず静かで、背丈を超える本棚に囲まれるているとちょっと贅沢な気分になれるので大好きです。またこの図書館は周囲に緑があるのも嬉しいです。皆さんのお住まいの地域にはどんな図書館がありますか?どの図書館にも趣、特色があって楽しいです。そうそう川崎市武蔵小杉の駅ビルに昨年オープンした図書館に行ってきました。奇麗で蔵書も多くシャレオツでした。以前武蔵小杉に住んでいたのですが、当時オープンしていたら絶対通っていたな〜と羨ましく思いました。そんなわけで、夏休みにお時間がある方は図書館に足を運んでみたらいかがでしょうか。リラックスして贅沢な気分になれるかもしれません:-)
さて、オセロの続きですね。ところで早くもXcode6のbeta5がリリースされてしまいました。リリースノートを眺めてみるとSwift言語自体にも機能追加があるようでした。Optional型の使い勝手向上は分かりましたが、その他の機能はよく分かりませんでした。これまでのbetaリリースは割と頻繁にXcodeがクラッシュしていたので、なるべく新しいリリース版を使うべきですが、とりあえず今回はbeta4で進めたいと思います。
まず最初の目標はオセロの盤面を画面に表示させることでした。オセロの盤面は8x8マスなので2次元配列で表現するのが自然に扱えそうです。ではSwiftの配列について少し実験しましょう。ちなみにSwiftの実験にはXcode6のPlaygroundという環境を使うと楽です。「リアルタイムのREPL」という感じで、コンパイルやRunさせなくても、コードを書いた瞬間に式の評価結果が画面に表示されるという代物です。とても便利です。頻繁なクラッシュがなければね〜ボソッ(-_-)
Swiftの配列はオブジェクトです。配列型はArray<T>と表現します。大括弧を使って[T]とも書けます。TはIntやStringなど配列の中身の型です。
var array1:Array<Int> = [1,2,3]
Int型の配列array1を定義してみました。varは変数宣言の識別子です。var変数は書き換え可能(mutable)です。以下のように異なる配列オブジェクトを参照するように書き換えることができます。
array1 = [4,5,6]
中身も書き換えることもできます。
array1[0] = 9
次は[]を使って文字列の配列を定義してみます。こっちの[]を使った型宣言の書き方のほうが分かりやすいですね。
let array2:[String] = ["one", "two", "three"]
letは定数宣言の識別子です。書き換え不可(immutable)です。参照値を書き換えたり、中身の変更もできません。
array2 = ["four", "five", "six"] // compile error
array2[0] = "nine" // compile error
上記例ではコロンの後ろにArray<Int>や[String]と型注釈を書いてコンパイラに変数の型を教えています。Swiftでは初期値を与えれば初期値の型から型推論できるので型注釈を省略できます。
var array1 = [1,2,3]
let array2 = ["one", "two", "three"]
配列はオブジェクトなのでいくつかの便利なメソッドやプロパティを備えています。例えば配列の要素数を返すプロパティがあります。
array1.count
=> 3
あとは関数型プログラミングのリスト操作でおなじみのトリオ、map,reduce,filter関数もあります。これらは関数を引数に渡す高階関数ってやつです。Swiftではクロージャを引数に渡すことになります。map関数は配列の各要素に対して、もらったクロージャを作用させて新しい配列を作ります。元の配列から新しい配列を作るだけで元の配列は何も変わりません。
var array2 = array1.map({(n:Int) -> Int in
return n * 2
})
array2
=> [2, 4, 8]
Swiftのクロージャは中括弧{}で括ります。inの前がクロージャの型です。上記例は(n:Int) -> Intなので、IntをもらってIntを返すクロージャです。inの後が処理です。もらった引数nを倍返ししています。
さて、ここからが面白いところです。Swiftの配列はオブジェクトですが、コピー動作時にプリミティブ値のように振る舞います。
var array1 = [1,2,3]
var array2 = array1
array1[0] = 100 // array1の内容を書き換えてみる・・・
array1
=> [100, 2, 3]
array2
=> [1, 2, 3] // array2は変わらない!!
ほ〜。JavaやC#のように配列をオブジェクトの参照として扱う言語とは異なります。ちょっと注意が必要ですね。これらの言語ではcopyやcloneなどのメソッドを呼び出して、明示的に新しい配列オブジェクトを生成する必要がありますが、Swiftでは不要です。配列をオブジェクトとして意識させるのではなく、「値」として扱わせるという観点は関数型プログラミングのアプローチと言えるでしょう。ただし、配列を常にプリミティブとして扱っていては不要なコピーコストが生じる可能性があります。コンパイル時にフロー解析をするのか、あるいは配列の中身の書き換えや要素数の変更時までコピーを遅延させているのか分かりませんが、いずれにせよプログラマが意識しないところでコピーコストの最小化が行われていると考えられます。なおSwiftでは文字列やディクショナリもプリミティブ値のように振る舞います。
文字列の例。
var str1 = "abc"
var str2 = str1
str1 += "def" // str1を書き換え
str1
=> “abcdef”
str2
=> “abc” // str2は変わらない
ディクショナリの例。
var person1 = ["age":17]
var person2 = person1
person2["age"] = 35 // person2を書き換え
person1
=> [“age”:17] // person1は変わらない
person2
=> [“age”:35]
さて話をオセロの盤面に戻します。2次元配列の中身の型はどうしましょう。各マスの取りうる状態としては黒石、白石、空きとなります。これらをオブジェクト(Enum)として表すこともできますが、今回は単純なInt値で表したいと思います。直値はやめて定数の名前ぐらいは用意しておきます。なお、Swiftはグローバルスコープやクラススコープに定義を書けるので、本記事で特に断りがなければグローバルスコープに定義を書いているものとします。クラススコープに記述する場合は、メンバ変数、メソッドなどと言うようにします。
let EMPTY = 0, BLACK_STONE = 1, WHITE_STONE = 2
盤面はメンバ変数として以下のように定義しました。Intの2次元配列です。Array<Array<Int>>と書いてもいいです。
class OthelloView: UIView {
var board:[[Int]]
}
ついでに盤面の初期状態を定数として定義しておきましょう。中央4マスにはあらかじめ石がおいてあります。右上が黒(1)です。以下の定義をよ〜く見ると10x10マスになっています。盤面の周囲を番兵値(0)でぐるりと囲んでいます。役に立つか分かりませんが、今後のアルゴリズムにおいて配列の範囲チェックを省略することが狙いです。よくある常套手段です。
let initboard = [
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,2,1,0,0,0,0],
[0,0,0,0,1,2,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0],
];
Viewの初期化時に上記の盤面初期値をboardメンバにコピーしましょう。前回OthelloViewをStoryboardに登録して簡単な動作確認をしましたが、Storyboardに登録したViewは初期化時に以下のイニシャライザが呼ばれます。
class OthelloView: UIView {
var board:[[Int]]
init(coder aDecoder: NSCoder!) {
}
}
このinitの中で盤面の初期化をします。それとNSCoderのinitは親クラスのイニシャライザを呼ぶ必要があります。
init (coder aDecoder: NSCoder!) {
board = initboard
super.init(coder:aDecoder)
}
盤面1マスの辺の長さ(size)と1列1行目の画面上の座標(top,left)を画面の大きさから計算します。
class OthelloView: UIView {
...
var side:CGFloat
var top:CGFloat
let left:CGFloat = 0
init(coder aDecoder: NSCoder!) {
let appFrame = UIScreen.mainScreen().applicationFrame // 画面の大きさ
side = appFrame.size.width / 8 // 画面幅の8等分が一辺の大きさ
top = (appFrame.size.height - (side * 8)) / 2
...
}
}
次は盤面の描画です。オーバーライドしたdrawRectの中でグラフィックスコンテキストを使うとViewに絵を描くことができます。まずは黒丸を書くコードでちょっと練習してみましょう。
override func drawRect(rect: CGRect) {
// グラフィックスコンテキストを取得
let context = UIGraphicsGetCurrentContext()
// 塗りつぶしの色に黒を設定
CGContextSetFillColorWithColor(context, UIColor.blackColor().CGColor)
// x:50, y:50 幅100,高さ100の正方形に内接する円を描く
CGContextFillEllipseInRect(context, CGRectMake(50, 50, 100, 100))
}
CG〜で始まる関数群はCoreGraphicsライブラリの関数です。基本的な手順は、(1)contextを取得して、(2)contextに線の太さや色を指定して、(3)描画関数を呼ぶ、これだけです。色オブジェクトは毎回描画時に生成するとコストがかかりますので、View初期化時に生成しておきます。3色用意しました。白、黒、盤面の緑です。緑はちょっと明るい色にしました。CoreGraphicsAPIに渡す色はCGColorオブジェクトですので、UIColorからCGColorに変換しておきます。
class OthelloView: UIView {
...
let white = UIColor.whiteColor().CGColor
let black = UIColor.blackColor().CGColor
let green = UIColor(red:0.6, green:1.0, blue:0.2, alpha:1.0).CGColor
...
}
ちょっと寄り道してSwiftのfor文の使い方を確認しましょう。C言語風の書き方で0〜9をコンソールに表示できます。iは書き変わるのでvarにします。
for var i = 0; i < 10; i++ {
println(i)
}
次のfor-in文は、他の言語ではfor-eachとか拡張for文と呼ばれているものです。inの後はイテレーションできる配列やRangeオブジェクトを書くことができます。
for i in 0..<10 {
println(i)
}
SwiftのRangeの書き方は、0..<10で10を含まない範囲(0〜9)、0...10で10を含む範囲(0〜10)となります。それでは以下に描画関数を一気に載せました。
override func drawRect(rect: CGRect) {
let context = UIGraphicsGetCurrentContext()
// 各マスの枠線は白で太さ1.5ピクセルに設定
CGContextSetStrokeColorWithColor(context, white)
CGContextSetLineWidth(context, 1.5)
for y in 1...8 {
for x in 1...8 {
// マスの位置からマスのrect領域を計算
let rx = left + side * CGFloat(x-1)
let ry = top + side * CGFloat(y-1)
let rect = CGRectMake(rx, ry, side, side)
// まずは盤面(正方形)を書くので塗りつぶし色を緑に設定
CGContextSetFillColorWithColor(context, green)
// 塗りつぶし(緑)、枠線(白)を描画
CGContextFillRect(context, rect)
CGContextStrokeRect(context, rect)
if board[y][x] == BLACK_STONE {
// 黒石が置いてあれば塗りつぶし色を黒にして円を描く
CGContextSetFillColorWithColor(context, black)
CGContextFillEllipseInRect(context, rect)
} else if board[y][x] == WHITE_STONE {
// 白石が置いてあれば塗りつぶし色を白にして円を描く
CGContextSetFillColorWithColor(context, white)
CGContextFillEllipseInRect(context, rect)
}
}
}
}
マスの位置計算がちょっと分かりにくいかもしれません。2重のfor文は番兵を除くため1〜8で変化させます。またboard[y][x]のx,yの順番に気をつけて下さい。xのループの方がyより早く回り、左から右へ、次に上から下へマスの塗りつぶしが進むイメージです。CGFloat()の部分はIntからCGFloatへの型キャストです。ここまででオセロの初期状態がiPhoneシミュレータに表示されるはずです。
(文字数制限にひっかかったので次の記事に続きます・・・)