Step 3: Janken UI - じゃんけんの画面を作ろう!
いよいよ本格的なじゃんけんアプリの画面を作ります。相手と自分の手を表示して、グー・チョキ・パーのボタンを配置しましょう!
3-1. 画像の準備
じゃんけんの手の画像を準備します。以下の画像をプロジェクトに追加してください:
- Assets.xcassetsを開く
- 以下の画像をドラッグ&ドロップで追加:
-
janken_gu.png
- グーの画像 -
janken_choki.png
- チョキの画像 -
janken_pa.png
- パーの画像 -
questionmark.png
- はてなマークの画像
3-2. コードを更新
ContentView.swift
を以下のコードに更新します:
import SwiftUI
struct ContentView: View {
// 自分の手を管理(最初は?マーク)
@State private var myHand = "questionmark"
// 相手の手を管理(最初は?マーク)
@State private var cpuHand = "questionmark"
var body: some View {
VStack(spacing: 20) {
// タイトル
Text("じゃんけんゲーム")
.font(.largeTitle)
.foregroundColor(.blue)
// 相手の手を表示
VStack {
Text("相手の手")
.font(.headline)
Image(systemName: cpuHand)
.font(.system(size: 80))
.foregroundColor(.orange)
}
.padding()
// 自分の手を表示
VStack {
Text("自分の手")
.font(.headline)
Image(myHand)
.resizable()
.frame(width: 150, height: 150)
}
.padding()
// じゃんけんボタンを横並びに配置
HStack(spacing: 30) {
// グーボタン
Button(action: {
myHand = "janken_gu"
}) {
VStack {
Text("ぐー")
.font(.system(size: 30))
}
}
// チョキボタン
Button(action: {
myHand = "janken_choki"
}) {
VStack {
Text("ちょき")
.font(.system(size: 30))
}
}
// パーボタン
Button(action: {
myHand = "janken_pa"
}) {
VStack {
Text("ぱー")
.font(.system(size: 30))
}
}
}
.padding()
}
}
}
#Preview {
ContentView()
}
3-3. 新しく追加した機能の解説
📱UIの構成
📦 HStack - 横並びレイアウト
HStack(spacing: 30) {
// ここに横に並べたいビューを書く
}
🖼️ Image - 画像の表示
アセット画像の表示:
Image(myHand)
.resizable() // サイズ変更可能にする
.frame(width: 150, height: 150) // サイズを指定
SF Symbolsの表示:
Image(systemName: cpuHand)
.font(.system(size: 80)) // サイズを指定
.foregroundColor(.orange) // 色を指定
🎮 複数の@State変数
@State private var myHand = "questionmark"
@State private var cpuHand = "questionmark"
- 自分の手と相手の手、それぞれ別々に管理
- 画像の名前を文字列として保存
- ボタンを押すと画像名が変わり、表示が切り替わる
🔘 カスタムボタン
Button(action: {
myHand = "janken_gu" // ボタンを押したときの処理
}) {
VStack {
Text("ぐー") // ボタンの見た目
.font(.system(size: 30))
}
}
3-4. 動作の流れ
- 最初は両方とも「?」マークが表示
- 「ぐー」ボタンをタップ
- myHandが"janken_gu"に変更
- 自分の手の画像がグーに切り替わる
- 相手の手はまだ「?」のまま(Step 4で実装)
Step 4: Game Logic - ゲームの頭脳を作ろう!
ついにゲームの核心部分です!コンピュータがランダムに手を選び、勝敗を判定する機能を実装します。
4-1. コードを更新
ContentView.swift
を以下のコードに更新します:
import SwiftUI
struct ContentView: View {
@State private var myHand = "questionmark"
@State private var cpuHand = "questionmark"
@State private var result = ""
// じゃんけんの手を管理
let hands = ["janken_gu", "janken_choki", "janken_pa"]
var body: some View {
VStack(spacing: 20) {
Text("じゃんけんゲーム")
.font(.largeTitle)
.foregroundColor(.blue)
VStack {
Text("相手の手")
.font(.headline)
// 相手の手を画像で表示
if cpuHand == "questionmark" {
Image(systemName: cpuHand)
.font(.system(size: 80))
.foregroundColor(.orange)
} else {
Image(cpuHand)
.resizable()
.frame(width: 150, height: 150)
}
}
.padding()
// 結果を表示
Text(result)
.font(.title)
.foregroundColor(resultColor)
.padding()
VStack {
Text("自分の手")
.font(.headline)
// 自分の手を画像で表示
if myHand == "questionmark" {
Image(systemName: myHand)
.font(.system(size: 80))
.foregroundColor(.green)
.frame(width: 150, height: 150)
} else {
Image(myHand)
.resizable()
.frame(width: 150, height: 150)
}
}
.padding()
HStack(spacing: 30) {
Button(action: {
playGame(myChoice: "janken_gu")
}) {
VStack {
Text("ぐー")
.font(.system(size: 30))
}
}
Button(action: {
playGame(myChoice: "janken_choki")
}) {
VStack {
Text("ちょき")
.font(.system(size: 30))
}
}
Button(action: {
playGame(myChoice: "janken_pa")
}) {
VStack {
Text("ぱー")
.font(.system(size: 30))
}
}
}
.padding()
}
}
// ゲームを実行する関数
func playGame(myChoice: String) {
// 自分の手を設定
myHand = myChoice
// CPUの手をランダムに決定
let cpuChoice = hands.randomElement()!
cpuHand = cpuChoice
// 勝敗を判定
result = judgeGame(myChoice: myChoice, cpuChoice: cpuChoice)
}
// 勝敗を判定する関数
func judgeGame(myChoice: String, cpuChoice: String) -> String {
// あいこの判定
if myChoice == cpuChoice {
return "あいこ"
}
// 勝敗の判定
if (myChoice == "janken_gu" && cpuChoice == "janken_choki") ||
(myChoice == "janken_choki" && cpuChoice == "janken_pa") ||
(myChoice == "janken_pa" && cpuChoice == "janken_gu") {
return "勝ち!"
} else {
return "負け..."
}
}
// 結果に応じた色を返す
var resultColor: Color {
switch result {
case "勝ち!":
return .blue
case "負け...":
return .red
case "あいこ":
return .green
default:
return .black
}
}
}
#Preview {
ContentView()
}
4-2. 新しく追加した機能の解説
🎯 関数 - 処理をまとめる
ゲーム実行関数:
func playGame(myChoice: String) {
// 1. 自分の手を設定
myHand = myChoice
// 2. CPUの手をランダムに決定
let cpuChoice = hands.randomElement()!
cpuHand = cpuChoice
// 3. 勝敗を判定
result = judgeGame(myChoice: myChoice, cpuChoice: cpuChoice)
}
- funcで関数を定義
- 処理を順番に実行
- ボタンから呼び出される
🎲 ランダム - コンピュータの手を決める
let hands = ["janken_gu", "janken_choki", "janken_pa"]
let cpuChoice = hands.randomElement()!
- 3つの手を配列に入れる
- randomElement()でランダムに1つ選ぶ
- !は「必ず値がある」という意味(配列が空じゃないから)
🏆 勝敗判定 - if文の活用
func judgeGame(myChoice: String, cpuChoice: String) -> String {
// まず、あいこかチェック
if myChoice == cpuChoice {
return "あいこ"
}
// 勝ちパターンを全部チェック
if (myChoice == "janken_gu" && cpuChoice == "janken_choki") ||
(myChoice == "janken_choki" && cpuChoice == "janken_pa") ||
(myChoice == "janken_pa" && cpuChoice == "janken_gu") {
return "勝ち!"
} else {
return "負け..."
}
}
じゃんけんのルール:
🎨 Computed Property - 自動計算される値
var resultColor: Color {
switch result {
case "勝ち!":
return .blue
case "負け...":
return .red
case "あいこ":
return .green
default:
return .black
}
}
- resultが変わると自動的に色も変わる
- 関数のように見えるけど、プロパティとして使える
4-3. ゲームの流れ
-
ボタンをタップ
例:「ぐー」ボタン -
playGame関数が呼ばれる
自分の手:janken_guに設定
CPUの手:ランダムに決定(例:janken_choki) -
judgeGame関数で判定
グー vs チョキ = 勝ち! -
画面が自動更新
Step 5: Final Design - プロ級の見た目に仕上げよう!
最後の仕上げです!これまで作ってきたじゃんけんアプリを、App Storeに並んでいてもおかしくないようなデザインにしていきます。
5-1. コードを更新
ContentView.swift
を以下のコードに更新します:
import SwiftUI
struct ContentView: View {
@State private var myHand = "questionmark"
@State private var cpuHand = "questionmark"
@State private var result = ""
// じゃんけんの手を管理
let hands = ["janken_gu", "janken_choki", "janken_pa"]
var body: some View {
VStack(spacing: 20) {
Text("じゃんけんゲーム")
.font(.largeTitle)
.fontWeight(.bold)
.foregroundColor(.blue)
.padding(.top)
VStack(spacing: 10) {
Text("相手の手")
.font(.headline)
.foregroundColor(.gray)
ZStack {
RoundedRectangle(cornerRadius: 15)
.fill(Color.orange.opacity(0.2))
.frame(width: 180, height: 180)
if cpuHand == "questionmark" {
Image(systemName: cpuHand)
.font(.system(size: 80))
.foregroundColor(.orange)
} else {
Image(cpuHand)
.resizable()
.scaledToFit()
.frame(width: 150, height: 150)
}
}
}
.padding()
// 結果を表示
if !result.isEmpty {
Text(result)
.font(.system(size: 40))
.fontWeight(.bold)
.foregroundColor(.white)
.padding(.horizontal, 30)
.padding(.vertical, 15)
.background(resultBackgroundColor)
.cornerRadius(25)
.shadow(radius: 5)
}
VStack(spacing: 10) {
Text("自分の手")
.font(.headline)
.foregroundColor(.gray)
ZStack {
RoundedRectangle(cornerRadius: 15)
.fill(Color.green.opacity(0.2))
.frame(width: 180, height: 180)
if myHand == "questionmark" {
Image(systemName: myHand)
.font(.system(size: 80))
.foregroundColor(.green)
} else {
Image(myHand)
.resizable()
.scaledToFit()
.frame(width: 150, height: 150)
}
}
}
.padding()
HStack(spacing: 20) {
Button(action: {
playGame(myChoice: "janken_gu")
}) {
VStack(spacing: 10) {
ZStack {
Circle()
.fill(Color.blue.opacity(0.2))
.frame(width: 80, height: 80)
Image("janken_gu")
.resizable()
.scaledToFit()
.frame(width: 60, height: 60)
}
Text("ぐー")
.font(.title2)
.fontWeight(.medium)
}
}
.buttonStyle(.borderedProminent)
.scaleEffect(myHand == "janken_gu" ? 1.1 : 1.0)
.animation(.easeInOut(duration: 0.1), value: myHand)
Button(action: {
playGame(myChoice: "janken_choki")
}) {
VStack(spacing: 10) {
ZStack {
Circle()
.fill(Color.blue.opacity(0.2))
.frame(width: 80, height: 80)
Image("janken_choki")
.resizable()
.scaledToFit()
.frame(width: 60, height: 60)
}
Text("ちょき")
.font(.title2)
.fontWeight(.medium)
}
}
.buttonStyle(.borderedProminent)
.scaleEffect(myHand == "janken_choki" ? 1.1 : 1.0)
.animation(.easeInOut(duration: 0.1), value: myHand)
Button(action: {
playGame(myChoice: "janken_pa")
}) {
VStack(spacing: 10) {
ZStack {
Circle()
.fill(Color.blue.opacity(0.2))
.frame(width: 80, height: 80)
Image("janken_pa")
.resizable()
.scaledToFit()
.frame(width: 60, height: 60)
}
Text("ぱー")
.font(.title2)
.fontWeight(.medium)
}
}
.buttonStyle(.borderedProminent)
.scaleEffect(myHand == "janken_pa" ? 1.1 : 1.0)
.animation(.easeInOut(duration: 0.1), value: myHand)
}
.padding()
Spacer()
}
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.gray.opacity(0.1))
.cornerRadius(20)
.padding()
}
// ゲームを実行する関数(Step 4と同じ)
func playGame(myChoice: String) {
myHand = myChoice
let cpuChoice = hands.randomElement()!
cpuHand = cpuChoice
result = judgeGame(myChoice: myChoice, cpuChoice: cpuChoice)
}
// 勝敗を判定する関数(Step 4と同じ)
func judgeGame(myChoice: String, cpuChoice: String) -> String {
if myChoice == cpuChoice {
return "あいこ"
}
if (myChoice == "janken_gu" && cpuChoice == "janken_choki") ||
(myChoice == "janken_choki" && cpuChoice == "janken_pa") ||
(myChoice == "janken_pa" && cpuChoice == "janken_gu") {
return "勝ち!"
} else {
return "負け..."
}
}
// 結果に応じた背景色を返す
var resultBackgroundColor: Color {
switch result {
case "勝ち!":
return .blue
case "負け...":
return .red
case "あいこ":
return .green
default:
return .clear
}
}
}
#Preview {
ContentView()
}
5-2. 新しく追加したデザイン要素の解説
🎨 余白とスペーシング
VStack(spacing: 20) { // 要素間の間隔を20に
// ...
}
.padding() // 標準の余白
.padding(.top) // 上だけに余白
- 適切な余白で見やすさアップ
- spacingで要素間の間隔を統一
🔲 ZStack - 重ね合わせレイアウト
ZStack {
// 背景の角丸四角形
RoundedRectangle(cornerRadius: 15)
.fill(Color.orange.opacity(0.2))
.frame(width: 180, height: 180)
// その上に画像
Image(cpuHand)
.resizable()
.scaledToFit()
.frame(width: 150, height: 150)
}
- ZStackは要素を重ねて表示
- 先に書いたものが後ろ、後に書いたものが前
🌈 透明度とカラー
Color.orange.opacity(0.2) // オレンジ色を20%の透明度に
- opacity(0.0):完全に透明
- opacity(0.5):半透明
- opacity(1.0):不透明
💫 アニメーション効果
.scaleEffect(myHand == "janken_gu" ? 1.1 : 1.0)
.animation(.easeInOut(duration: 0.1), value: myHand)
- 選択したボタンが1.1倍に拡大
- 0.1秒かけてスムーズに変化
- easeInOut:始めと終わりがゆっくり
🎭 影効果
.shadow(radius: 5)
- 要素に立体感を追加
- radius:影のぼかし具合
🏷️ 条件付き表示
if !result.isEmpty {
Text(result)
// 結果があるときだけ表示
}
- 不要な空白を避ける
- スッキリした見た目
Complete版 - さらに機能を追加しよう!
基本的なじゃんけんアプリが完成したら、もっと楽しい機能を追加してみましょう!対戦成績の記録、アニメーション、効果音など、プロ級のアプリに仕上げていきます。
Complete版の完成イメージ
Complete版では以下の機能を追加します:
- 📊 対戦成績の記録と表示
- 🔄 リセットボタン
- ✨ 豪華なアニメーション効果
- 🎨 より洗練されたデザイン
Complete版のコード
ContentView.swift
を以下のコードに更新します:
import SwiftUI
struct ContentView: View {
@State private var myHand = "questionmark"
@State private var cpuHand = "questionmark"
@State private var result = ""
// 対戦成績を記録
@State private var wins = 0
@State private var losses = 0
@State private var draws = 0
// アニメーション用のフラグ
@State private var isAnimating = false
@State private var showResult = false
let hands = ["janken_gu", "janken_choki", "janken_pa"]
var body: some View {
ZStack {
// グラデーション背景
LinearGradient(
gradient: Gradient(colors: [Color.blue.opacity(0.1), Color.purple.opacity(0.1)]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
.ignoresSafeArea()
VStack(spacing: 20) {
// タイトルと成績
VStack(spacing: 10) {
Text("じゃんけんゲーム")
.font(.largeTitle)
.fontWeight(.bold)
.foregroundColor(.blue)
// 成績表示
ScoreView(wins: wins, losses: losses, draws: draws)
}
.padding(.top)
// 相手の手
VStack(spacing: 10) {
Text("相手の手")
.font(.headline)
.foregroundColor(.gray)
ZStack {
RoundedRectangle(cornerRadius: 15)
.fill(Color.orange.opacity(0.2))
.frame(width: 180, height: 180)
.shadow(color: .orange.opacity(0.3), radius: 10)
if cpuHand == "questionmark" {
Image(systemName: cpuHand)
.font(.system(size: 80))
.foregroundColor(.orange)
} else {
Image(cpuHand)
.resizable()
.scaledToFit()
.frame(width: 150, height: 150)
.rotationEffect(Angle(degrees: isAnimating ? 360 : 0))
.animation(.easeInOut(duration: 0.5), value: isAnimating)
}
}
}
// 結果表示(アニメーション付き)
if !result.isEmpty {
Text(result)
.font(.system(size: 40))
.fontWeight(.bold)
.foregroundColor(.white)
.padding(.horizontal, 30)
.padding(.vertical, 15)
.background(resultBackgroundColor)
.cornerRadius(25)
.shadow(radius: 5)
.scaleEffect(showResult ? 1.0 : 0.1)
.animation(.spring(response: 0.5, dampingFraction: 0.6), value: showResult)
}
// 自分の手
VStack(spacing: 10) {
Text("自分の手")
.font(.headline)
.foregroundColor(.gray)
ZStack {
RoundedRectangle(cornerRadius: 15)
.fill(Color.green.opacity(0.2))
.frame(width: 180, height: 180)
.shadow(color: .green.opacity(0.3), radius: 10)
if myHand == "questionmark" {
Image(systemName: myHand)
.font(.system(size: 80))
.foregroundColor(.green)
} else {
Image(myHand)
.resizable()
.scaledToFit()
.frame(width: 150, height: 150)
}
}
}
// じゃんけんボタン
HStack(spacing: 20) {
GameButton(
imageName: "janken_gu",
text: "ぐー",
isSelected: myHand == "janken_gu",
action: { playGame(myChoice: "janken_gu") }
)
GameButton(
imageName: "janken_choki",
text: "ちょき",
isSelected: myHand == "janken_choki",
action: { playGame(myChoice: "janken_choki") }
)
GameButton(
imageName: "janken_pa",
text: "ぱー",
isSelected: myHand == "janken_pa",
action: { playGame(myChoice: "janken_pa") }
)
}
.padding()
// リセットボタン
Button(action: resetGame) {
Label("リセット", systemImage: "arrow.clockwise")
.font(.headline)
.foregroundColor(.white)
.padding(.horizontal, 20)
.padding(.vertical, 10)
.background(Color.red)
.cornerRadius(20)
}
.disabled(wins == 0 && losses == 0 && draws == 0)
.opacity(wins == 0 && losses == 0 && draws == 0 ? 0.5 : 1.0)
Spacer()
}
.padding()
}
}
// ゲームを実行する関数(アニメーション追加)
func playGame(myChoice: String) {
// リセット
showResult = false
isAnimating = true
// 自分の手を設定
myHand = myChoice
// アニメーション付きでCPUの手を決定
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
let cpuChoice = hands.randomElement()!
cpuHand = cpuChoice
// 勝敗を判定
result = judgeGame(myChoice: myChoice, cpuChoice: cpuChoice)
// 成績を更新
updateScore()
// 結果を表示
withAnimation {
showResult = true
isAnimating = false
}
}
}
// 勝敗を判定する関数
func judgeGame(myChoice: String, cpuChoice: String) -> String {
if myChoice == cpuChoice {
return "あいこ"
}
if (myChoice == "janken_gu" && cpuChoice == "janken_choki") ||
(myChoice == "janken_choki" && cpuChoice == "janken_pa") ||
(myChoice == "janken_pa" && cpuChoice == "janken_gu") {
return "勝ち!"
} else {
return "負け..."
}
}
// 成績を更新する関数
func updateScore() {
switch result {
case "勝ち!":
wins += 1
case "負け...":
losses += 1
case "あいこ":
draws += 1
default:
break
}
}
// ゲームをリセットする関数
func resetGame() {
withAnimation {
wins = 0
losses = 0
draws = 0
myHand = "questionmark"
cpuHand = "questionmark"
result = ""
showResult = false
}
}
// 結果に応じた背景色
var resultBackgroundColor: Color {
switch result {
case "勝ち!":
return .blue
case "負け...":
return .red
case "あいこ":
return .green
default:
return .clear
}
}
}
// 成績表示用のビュー
struct ScoreView: View {
let wins: Int
let losses: Int
let draws: Int
var body: some View {
HStack(spacing: 20) {
VStack {
Text("勝ち")
.font(.caption)
.foregroundColor(.blue)
Text("\(wins)")
.font(.title2)
.fontWeight(.bold)
.foregroundColor(.blue)
}
VStack {
Text("負け")
.font(.caption)
.foregroundColor(.red)
Text("\(losses)")
.font(.title2)
.fontWeight(.bold)
.foregroundColor(.red)
}
VStack {
Text("あいこ")
.font(.caption)
.foregroundColor(.green)
Text("\(draws)")
.font(.title2)
.fontWeight(.bold)
.foregroundColor(.green)
}
}
.padding()
.background(Color.white.opacity(0.8))
.cornerRadius(15)
.shadow(radius: 3)
}
}
// ゲームボタン用のビュー
struct GameButton: View {
let imageName: String
let text: String
let isSelected: Bool
let action: () -> Void
var body: some View {
Button(action: action) {
VStack(spacing: 10) {
ZStack {
Circle()
.fill(Color.blue.opacity(0.2))
.frame(width: 80, height: 80)
.shadow(color: .blue.opacity(0.3), radius: isSelected ? 10 : 5)
Image(imageName)
.resizable()
.scaledToFit()
.frame(width: 60, height: 60)
}
Text(text)
.font(.title2)
.fontWeight(.medium)
}
}
.buttonStyle(.borderedProminent)
.scaleEffect(isSelected ? 1.15 : 1.0)
.animation(.spring(response: 0.3, dampingFraction: 0.6), value: isSelected)
}
}
#Preview {
ContentView()
}
📊 対戦成績の記録
@State private var wins = 0
@State private var losses = 0
@State private var draws = 0
- 各結果をカウント
- ScoreViewで見やすく表示
- リアルタイムで更新
🎬 アニメーション効果
結果表示のスプリングアニメーション:
.scaleEffect(showResult ? 1.0 : 0.1)
.animation(.spring(response: 0.5, dampingFraction: 0.6), value: showResult)
- 小さい状態から弾むように拡大
- springで自然な動き
CPUの手の回転:
.rotationEffect(Angle(degrees: isAnimating ? 360 : 0))
.animation(.easeInOut(duration: 0.5), value: isAnimating)
⏱️ 遅延実行
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
// 0.5秒後に実行
}
- じらし効果で盛り上がる
- CPUが考えているような演出
🧩 カスタムコンポーネント
ScoreView
- 成績を見やすく表示
- 再利用可能な部品
GameButton
- ボタンのデザインを統一
- コードの重複を削減
これで、あなたも立派なiOSアプリ開発者の仲間入りです!
🌟 最後に
プログラミングは「作りながら学ぶ」のが一番です。今回作ったアプリをベースに、自分だけのオリジナル機能を追加してみてください。