3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SwiftUIで作るじゃんけんアプリ - 1年生向けハンズオン(後編)

Posted at

Step 3: Janken UI - じゃんけんの画面を作ろう!

いよいよ本格的なじゃんけんアプリの画面を作ります。相手と自分の手を表示して、グー・チョキ・パーのボタンを配置しましょう!

3-1. 画像の準備

じゃんけんの手の画像を準備します。以下の画像をプロジェクトに追加してください:
janken_pa.png
janken_gu.png
janken_choki.png

  1. Assets.xcassetsを開く
  2. 以下の画像をドラッグ&ドロップで追加:
  • 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の構成

スクリーンショット 2025-07-01 1.33.52.png

📦 HStack - 横並びレイアウト

HStack(spacing: 30) {
    // ここに横に並べたいビューを書く
}
  • HStackは Horizontal Stack(水平スタック)の略
  • spacing: 30で要素間の間隔を30ポイントに設定
  • VStackは縦、HStackは横と覚えましょう!
    スクリーンショット 2025-07-01 1.33.45.png

🖼️ Image - 画像の表示

アセット画像の表示:
Image(myHand)
    .resizable()              // サイズ変更可能にする
    .frame(width: 150, height: 150)  // サイズを指定
  • 「いらすとや」で用意したアセット画像
    スクリーンショット 2025-07-01 0.47.28.png
SF Symbolsの表示:
Image(systemName: cpuHand)
    .font(.system(size: 80))  // サイズを指定
    .foregroundColor(.orange) // 色を指定
  • appleの公式が用意してくれているアイコン画像
    スクリーンショット 2025-07-01 0.47.15.png

🎮 複数の@State変数

@State private var myHand = "questionmark"
@State private var cpuHand = "questionmark"
  • 自分の手と相手の手、それぞれ別々に管理
  • 画像の名前を文字列として保存
  • ボタンを押すと画像名が変わり、表示が切り替わる

🔘 カスタムボタン

Button(action: {
    myHand = "janken_gu"  // ボタンを押したときの処理
}) {
    VStack {
        Text("ぐー")      // ボタンの見た目
            .font(.system(size: 30))
    }
}

3-4. 動作の流れ

  1. 最初は両方とも「?」マークが表示
  2. 「ぐー」ボタンをタップ
  3. myHandが"janken_gu"に変更
  4. 自分の手の画像がグーに切り替わる
  5. 相手の手はまだ「?」のまま(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 "負け..."
    }
}

じゃんけんのルール:

グー > チョキ
チョキ > パー
パー > グー
スクリーンショット 2025-07-01 1.49.01.png

🎨 Computed Property - 自動計算される値

var resultColor: Color {
    switch result {
    case "勝ち!":
        return .blue
    case "負け...":
        return .red
    case "あいこ":
        return .green
    default:
        return .black
    }
}
  • resultが変わると自動的に色も変わる
  • 関数のように見えるけど、プロパティとして使える

4-3. ゲームの流れ

スクリーンショット 2025-07-01 2.03.00.png

  1. ボタンをタップ
    例:「ぐー」ボタン

  2. playGame関数が呼ばれる
    自分の手:janken_guに設定
    CPUの手:ランダムに決定(例:janken_choki)

  3. judgeGame関数で判定
    グー vs チョキ = 勝ち!

  4. 画面が自動更新

  5. 両方の手が表示
    「勝ち!」が青色で表示
    スクリーンショット 2025-07-01 1.52.12.png

Step 5: Final Design - プロ級の見た目に仕上げよう!

最後の仕上げです!これまで作ってきたじゃんけんアプリを、App Storeに並んでいてもおかしくないようなデザインにしていきます。

スクリーンショット 2025-07-01 2.18.06.png

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版の完成イメージ

スクリーンショット 2025-07-01 2.28.16.png

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アプリ開発者の仲間入りです!

🌟 最後に

プログラミングは「作りながら学ぶ」のが一番です。今回作ったアプリをベースに、自分だけのオリジナル機能を追加してみてください。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?