本投稿は TECOTEC Advent Calendar 2022 の20日目の記事です。
こんにちは決済認証システム開発事業部の大﨑です。
実務ではまだSwiftUIでアプリを作ったことがないのですが、今後増えるだろうと思い密かに勉強はしていました。
最近SwiftUIで作られたアプリをレビューしている中で、これなら簡単にできるのではないかと思い立ったのがクロスワードパズルです!(若干クリスマス感もあるし!)
細かい部分の記述やマジックナンバーなど突っ込みどころは多いですがご容赦ください。
今回作るのはこんな感じの4×4の簡単なクロスワードです!
5×5やそれより大きいのもできるのですが、問題を作るのが大変なので一番簡単な4×4にしました(笑)
今回一番苦労したのは問題を作るところでした。。
まず用意する配列は全部で4つ
- タップした文字を保持するための配列
- クロスワード完成形の配列
- タップした場所によって問題を表示する用の配列
- タップして入力できる文字の配列
// タップした文字を保持するための配列 @State var crossWords = [ ["", "", "", ""], ["-", "", "-", ""], ["", "", "", ""], ["", "-", "", ""] ] // クロスワードの完成形 let successWords = [ ["し", "ま", "う", "ま"], ["-", "ん", "-", "つ"], ["お", "と", "さ", "た"], ["に", "-", "い", "け"] ] // 問題を表示用 let questions = [ ["①横:白黒の模様を持つ馬", "②縦:袖が無い外套の一種 ヒーローや騎士が身につけること多い", "", "③縦:秋の味覚とされる高級なキノコ"], ["", "", "", ""], ["④横:便り。連絡。 〇〇〇〇がない\n 縦:想像上の怪物 〇〇ごっこで捕まえる役", "", "⑤縦:ゾウに次ぐ大型の陸上哺乳類 頭部に1本か2本の太い角をもっている", ""], ["", "", "⑥横:くぼ地に水がたまった所 湖や沼より小さい", ""] ] // 入力できる文字の設定 let inputWords = [ ["ら", "や", "ま", "は", "な", "た", "さ", "か", "あ"], ["り", "わ", "み", "ひ", "に", "ち", "し", "き", "い"], ["る", "ゆ", "む", "ふ", "ぬ", "つ", "す", "く", "う"], ["れ", "ん", "め", "へ", "ね", "て", "せ", "け", "え"], ["ろ", "よ", "も", "ほ", "の", "と", "そ", "こ", "お"] ]
次にメインとなるクロスワードの表示部分
VStack と HStack でマスの分描画します。
// クロスワードの表示エリア VStack(spacing: 0) { ForEach(0 ..< 4) { y in HStack(spacing: 0) { ForEach(0 ..< 4) { x in if crossWords[y][x] != "-" { ZStack { Button(action: { selectedX = x selectedY = y }) { Text(crossWords[y][x]) .frame(maxWidth: .infinity, maxHeight: .infinity) .border(.black) .background((selectedY == y && selectedX == x) ? .yellow : .white) .foregroundColor(Color.black) .font(Font.system(size: 30).bold()) } // マス内の左上に問題番号を表示する Text(questions[y][x].prefix(1)) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) } } else { // 選択できない箇所は背景を黒にする Spacer() .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color.black) } } .frame(width: 80) } } .frame(height: 80) } .border(.black, width: 2)
左上に数字があるマスをタップしたときに問題を表示します。
// 問題の表示エリア VStack() { if questions[selectedY][selectedX] != "" { Text(questions[selectedY][selectedX]) .frame(maxWidth:.infinity, alignment: .leading) } } .frame(height: 80)
最後にタップできる文字をボタンで配置します。
入りきらない部分は横ScrollViewにしています。
余談ですがUIKitでScrollViewを実装するのは大変でしたが、SwiftUIではだいぶ簡単になりましたよね!
ボタンタップすると選択しているマスに文字を表示します。
// 入力文字の表示エリア ScrollView([.horizontal]) { VStack(spacing: 5) { ForEach(0 ..< 5) { i in HStack(spacing: 5) { ForEach(0 ..< 9) { j in Button(action: { // タップした文字をクロスワード上に反映する crossWords[selectedY][selectedX] = inputWords[i][j] // 完成形の配列と同じになったら成功アラートを表示 if crossWords == successWords { self.successAlert = true } }) { Text(inputWords[i][j]) .frame(width: 50, height: 50) .background(Color(red: 248/255, green: 248/255, blue: 248/255)) .border(.gray) .foregroundColor(Color.black) } .alert(isPresented: $successAlert) { Alert(title: Text("おめでとう!完成です!")) } } } } } }
こちらがクロスワードが完成したときのイメージです!
以下コード全体です。
import SwiftUI struct ContentView: View { // タップした文字を保持するための配列 @State var crossWords = [ ["", "", "", ""], ["-", "", "-", ""], ["", "", "", ""], ["", "-", "", ""] ] // クロスワードの完成形 let successWords = [ ["し", "ま", "う", "ま"], ["-", "ん", "-", "つ"], ["お", "と", "さ", "た"], ["に", "-", "い", "け"] ] // 問題を表示用 let questions = [ ["①横:白黒の模様を持つ馬", "②縦:袖が無い外套の一種 ヒーローや騎士が身につけること多い", "", "③縦:秋の味覚とされる高級なキノコ"], ["", "", "", ""], ["④横:便り。連絡。 〇〇〇〇がない\n 縦:想像上の怪物 〇〇ごっこで捕まえる役", "", "⑤縦:ゾウに次ぐ大型の陸上哺乳類 頭部に1本か2本の太い角をもっている", ""], ["", "", "⑥横:くぼ地に水がたまった所 湖や沼より小さい", ""] ] // 入力できる文字の設定 let inputWords = [ ["ら", "や", "ま", "は", "な", "た", "さ", "か", "あ"], ["り", "わ", "み", "ひ", "に", "ち", "し", "き", "い"], ["る", "ゆ", "む", "ふ", "ぬ", "つ", "す", "く", "う"], ["れ", "ん", "め", "へ", "ね", "て", "せ", "け", "え"], ["ろ", "よ", "も", "ほ", "の", "と", "そ", "こ", "お"] ] // 選択中のボックス @State var selectedX = 0 @State var selectedY = 0 @State var successAlert = false var body: some View { VStack(spacing: 0) { // クロスワードの表示エリア VStack(spacing: 0) { ForEach(0 ..< 4) { y in HStack(spacing: 0) { ForEach(0 ..< 4) { x in if crossWords[y][x] != "-" { ZStack { Button(action: { selectedX = x selectedY = y }) { Text(crossWords[y][x]) .frame(maxWidth: .infinity, maxHeight: .infinity) .border(.black) .background((selectedY == y && selectedX == x) ? .yellow : .white) .foregroundColor(Color.black) .font(Font.system(size: 30).bold()) } // マス内の左上に問題番号を表示する Text(questions[y][x].prefix(1)) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) } } else { // 選択できない箇所は背景を黒にする Spacer() .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color.black) } } .frame(width: 80) } } .frame(height: 80) } .border(.black, width: 2) // 問題の表示エリア VStack() { if questions[selectedY][selectedX] != "" { Text(questions[selectedY][selectedX]) .frame(maxWidth:.infinity, alignment: .leading) } } .frame(height: 80) // 入力文字の表示エリア ScrollView([.horizontal]) { VStack(spacing: 5) { ForEach(0 ..< 5) { i in HStack(spacing: 5) { ForEach(0 ..< 9) { j in Button(action: { // タップした文字をクロスワード上に反映する crossWords[selectedY][selectedX] = inputWords[i][j] // 完成形の配列と同じになったら成功アラートを表示 if crossWords == successWords { self.successAlert = true } }) { Text(inputWords[i][j]) .frame(width: 50, height: 50) .background(Color(red: 248/255, green: 248/255, blue: 248/255)) .border(.gray) .foregroundColor(Color.black) } .alert(isPresented: $successAlert) { Alert(title: Text("おめでとう!完成です!")) } } } } } } } } }
SwiftUIであれば、100行ちょっとのコードでクロスワードパズルができるので、UIKitでやっていた頃と比べると実装コストも雲泥の差になりますね。
もちろん凝ったゲームはゲーム開発用エンジンを使った方がいいのですが、SwiftUIでも内容次第で簡単にゲームが作れるんだなと感動しました!
他には数独とか単純な知育アプリであれば、SwiftUIでも出来そうな気がします。
それに、ちょっとした遊び心があれば勉強も楽しくなりますからね!
では、良いクリスマスをお過ごしください!