本投稿は 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でも出来そうな気がします。
それに、ちょっとした遊び心があれば勉強も楽しくなりますからね!
では、良いクリスマスをお過ごしください!