見出し画像

ChatGPTに、クイズアプリの機能追加をしてもらう(swiftUI,iPhoneアプリ)

前回クイズアプリをChatGPTに作ってもらいましたが、その続きで、タイマー、進捗バー、スコア、回答時の丸バツを追加していこうと思いますー

前回のクイズアプリ作成記事

ChatGPTにどのような機能を追加したら良いかのアドバイスをもらう

質問文と選択肢以外に、どのような機能を追加した方が良いかを、ChatGPTに聞いてみます。それで追加しやすそうなアイディアを今回追加していこうかと思います。

ChatGPTの回答

4択クイズアプリに配置するその他の要素として、以下のアイデアを検討できます:
タイマー
質問ごとに制限時間を設け、ユーザーに残り時間を表示します。これにより、緊張感とゲーム性が増します。
ヒントボタン
ヒントを表示するボタンを追加し、必要なときにユーザーが助けを求められるようにします。ヒントを使用すると、ポイントが減るなどのペナルティを設定することもできます。
スコアボード
現在のスコアや累積スコアを表示するエリアを設け、ユーザーが自分の成績を確認できるようにします。
フィードバックメッセージ
各質問に対する正解・不正解のフィードバックを表示し、ユーザーが自分の答えを確認できるようにします。
進行状況バー
全体のクイズの進行状況を示すバーやアイコンを配置し、ユーザーがどこまで進んだかを視覚的に理解できるようにします。
次の質問ボタン
質問に回答した後、次の質問に進むためのボタンを配置します。
サウンドエフェクト
正解や不正解の際にサウンドエフェクトを追加し、ユーザー体験を向上させます。
設定メニュー
アプリの設定を調整するためのメニューを用意し、音量調整やテーマ変更などのオプションを提供します。

これらの要素を組み合わせて、ユーザーにとって楽しくて使いやすいアプリをデザインすることができます。

ChatGPTに機能追加用のコードを教えてもらう

プロンプト

以下の機能を追加したいです
回答時に正解不正解を示す、進行状況バー、スコアボード、タイマー、
選択肢ボタン押下時に、 正解の場合はアニメーションで丸印を示し 不正解の場合はアニメーションでバツ印を示したい

ChatGPTの回答

import SwiftUI

class QuizViewModel: ObservableObject {
    @Published var questions: [QuizQuestion] = []
    @Published var currentQuestionIndex = 0
    @Published var score = 0
    @Published var answerStatus: String? = nil  // 正解不正解を示す
    @Published var timerValue: Int = 10  // タイマーの初期値
    @Published var showResultIcon: Bool = false  // 結果アイコンの表示フラグ
    @Published var isCorrect: Bool = false  // 正解かどうかのフラグ
    var timer: Timer?  // タイマーインスタンス

    init() {
        loadCSV()
    }

    func loadCSV() {
        // CSVファイルを読み込み、questions配列に追加
        // (読み込みコードは省略)

        questions.shuffle()
        startTimer()
    }

    func answerQuestion(with answer: String) {
        if questions[currentQuestionIndex].correct == answer {
            score += 1
            isCorrect = true
        } else {
            isCorrect = false
        }
        withAnimation(.easeInOut(duration: 0.5)) {
            showResultIcon = true
        }
        DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
            withAnimation(.easeInOut(duration: 0.5)) {
                showResultIcon = false
            }
            self.currentQuestionIndex += 1
            if self.currentQuestionIndex < self.questions.count {
                self.startTimer()  // 次の質問のためにタイマーをリセット
            }
        }
    }

    func restartQuiz() {
        score = 0
        currentQuestionIndex = 0
        answerStatus = nil
        questions.shuffle()
        startTimer()
    }

    func startTimer() {
        timer?.invalidate()
        timerValue = 10
        timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
            if self.timerValue > 0 {
                self.timerValue -= 1
            } else {
                self.answerQuestion(with: "")  // タイムアウト時は空の回答を送信
            }
        }
    }
}

struct QuizView: View {
    @ObservedObject var viewModel = QuizViewModel()

    var body: some View {
        ZStack {
            VStack {
                ProgressView(value: Double(viewModel.currentQuestionIndex), total: Double(viewModel.questions.count))
                    .padding()

                if let answerStatus = viewModel.answerStatus {
                    Text(answerStatus)
                        .font(.headline)
                        .foregroundColor(answerStatus == "Correct!" ? .green : .red)
                }

                Text(viewModel.questions[viewModel.currentQuestionIndex].question)
                    .font(.title)
                    .padding()

                ForEach(viewModel.questions[viewModel.currentQuestionIndex].options, id: \.self) { option in
                    Button(action: {
                        viewModel.answerQuestion(with: option)
                    }) {
                        Text(option)
                            .frame(maxWidth: .infinity)
                            .padding()
                            .background(Color.blue)
                            .foregroundColor(.white)
                            .cornerRadius(10)
                    }
                    .padding(.horizontal)
                }

                HStack {
                    Text("Score: \(viewModel.score)")
                    Spacer()
                    Text("Timer: \(viewModel.timerValue)")
                }
                .padding()
            }
            .navigationTitle("Quiz")
            .padding()

            if viewModel.showResultIcon {
                ResultIconView(isCorrect: viewModel.isCorrect)
                    .transition(.scale)
                    .animation(.easeInOut(duration: 0.5), value: viewModel.showResultIcon)
            }
        }
    }
}

struct ResultIconView: View {
    let isCorrect: Bool

    var body: some View {
        Group {
            if isCorrect {
                Circle()
                    .stroke(Color.green, lineWidth: 10)
                    .frame(width: 100, height: 100)
            } else {
                XMark()
                    .stroke(Color.red, lineWidth: 10)
                    .frame(width: 100, height: 100)
            }
        }
    }
}

struct XMark: Shape {
    func path(in rect: CGRect) -> Path {
        var path = Path()
        path.move(to: CGPoint(x: rect.minX, y: rect.minY))
        path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
        path.move(to: CGPoint(x: rect.maxX, y: rect.minY))
        path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
        return path
    }
}

struct ContentView: View {
    var body: some View {
        NavigationView {
            VStack {
                NavigationLink(destination: QuizView()) {
                    Text("Start Quiz")
                        .foregroundColor(.white)
                        .frame(width: UIScreen.main.bounds.width * 0.9)
                        .padding()
                        .background(Color.blue)
                        .cornerRadius(10)
                }
                .padding()
            }
            .navigationTitle("Main View")
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

動作確認

画面イメージは以下で、スコアやタイマーと進捗バーが追加されました!
(スクショには載っていませんが回答時の丸バツマークも実装されています。)

ただスコアやタイマーは、もうちょっとイイ感じのレイアウトを期待していましたが、特段プロンプトでオーダーしていないため、最低限の機能としての実装となっているようです。


前回使用したプロンプトを使用すると、コードが一部省略される

前回のクイズアプリ作成時に使用したプロンプトの続きに、プロンプトを入力したせいか、ロジックは一部省略されており、ロジックを補完するのに少々手間がかかってしまいました。

ChatGPTの回答は、多少バグもある

それとタイマーを発動する処理が、CSVファイル読み込み時に発動するロジックとなっていたので、クイズ画面表示前にタイマー処理が動いてしまっており、正しく動作しませんでした。

    func loadCSV() {
        // CSVファイルを読み込み、questions配列に追加
        // (読み込みコードは省略)

        questions.shuffle()
        startTimer()
    }

理解していないロジックについては都度把握するようにする

ChatGPTは高速でコーディングできてすごく便利ですが、多少バグも含まれているので、理解していないロジックについては都度把握するようにしていくのが良いかと思います。ChatGPTがすぐに答えてくれますし!

ロジックの詳細をChatGPTに聞いて見る

ChatGPTに教えてもらったロジックについて整理したり、不明点はChatGPTに聞いて理解を深めていきたいと思います。

view

画面構成を整理すると以下のようなイメージ。

    var body: some View {
        ZStack {
            VStack {
                // 進行バー
                // 正解不正解ラベル
                // 質問文
                // 選択肢
                // スコアとタイマー
            }

            if viewModel.showResultIcon {
                // マルバツのアニメーション
            }
        }
    }

選択肢ボタンを押下した時に発火するファンクション

    func answerQuestion(with answer: String) {
        if questions[currentQuestionIndex].correct == answer {
            score += 1
            isCorrect = true
        } else {
            isCorrect = false
        }
        withAnimation(.easeInOut(duration: 0.5)) {
            showResultIcon = true
        }
        DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
            withAnimation(.easeInOut(duration: 0.5)) {
                showResultIcon = false
            }
            self.currentQuestionIndex += 1
            if self.currentQuestionIndex < self.questions.count {
                self.startTimer()  // 次の質問のためにタイマーをリセット
            }
        }
    }

アニメーションで結果を表示

withAnimation(.easeInOut(duration: 0.5)) {
    showResultIcon = true
}

withAnimation(.easeInOut(duration: 0.5)):
アニメーションを適用するためのブロック。0.5秒のイーズイン・イーズアウトアニメーションが設定されています。
showResultIcon = true:
結果アイコンを表示するためのフラグを設定します。

DispatchQueue.main.asyncAfterを使用して、1秒後に次の処理を実行

DispatchQueue.main.asyncAfter(deadline: .now() + 1.0)

目的: 指定された遅延時間(ここでは1秒)後に非同期でブロック内のコードを実行します。
DispatchQueue.main: メインスレッドのキューです。ユーザーインターフェイスの更新はメインスレッドで行う必要があるため、メインキューを使用します。
asyncAfter(deadline:): 指定した時間が経過した後に非同期でコードを実行するメソッドです。deadlineパラメータには、現在の時間に1秒を加えた時刻を指定しています。

withAnimationを使用して、アニメーションで結果アイコンを非表示

withAnimation(.easeInOut(duration: 0.5)) {
    showResultIcon = false
}

目的: 状態変更をアニメーションで行います。
withAnimation: 状態変更をアニメーション付きで行うための関数です。ここでは、0.5秒のイーズイン・イーズアウトアニメーションが指定されています。
showResultIcon = false: showResultIconプロパティをfalseに設定します。これにより、表示されていた結果アイコンがアニメーションで非表示になります。

クイズの進行状況バー

ProgressView(value: Double(viewModel.currentQuestionIndex), total: Double(viewModel.questions.count))
     .padding()

ProgressView: 進行状況を表示するためのSwiftUIのビューコンポーネントです。バーの形で進行状況を視覚的に示します。
value: 現在の進行状況を表す値です。ここでは、現在の質問のインデックスを示します。viewModel.currentQuestionIndexが進行状況を示すための値として使用されています。
Double(viewModel.currentQuestionIndex): currentQuestionIndexは通常整数ですが、ProgressViewのvalueはDouble型であるため、Doubleにキャストしています。
total: 進行状況の最大値を表します。ここでは、総質問数を示します。viewModel.questions.countが全体の質問数を示すための値として使用されています。
Double(viewModel.questions.count): questions.countは通常整数ですが、ProgressViewのtotalもDouble型であるため、Doubleにキャストしています。

回答時に丸バツマークを描画するロジック

struct ResultIconView: View {
    let isCorrect: Bool
    
    var body: some View {
        Group {
            if isCorrect {
                Circle()
                    .stroke(Color.green, lineWidth: 10)
                    .frame(width: 100, height: 100)
            } else {
                XMark()
                    .stroke(Color.red, lineWidth: 10)
                    .frame(width: 100, height: 100)
            }
        }
    }
}

マル印の描画メソッドは標準で用意されているが、バツ印は自分で実装

緑色の円
Circle():
円形のシェイプを表示します。
stroke(Color.green, lineWidth: 10):
円の輪郭を緑色で描画し、線幅を10ポイントに設定します。
frame(width: 100, height: 100):
円のサイズを幅100ポイント、高さ100ポイントに設定します。


赤色のバツ印
XMark():
カスタムシェイプで、バツ印(Xマーク)を表示します。XMarkは別の構造体として定義されている必要があります。
stroke(Color.red, lineWidth: 10):
バツ印の輪郭を赤色で描画し、線幅を10ポイントに設定します。
frame(width: 100, height: 100):
バツ印のサイズを幅100ポイント、高さ100ポイントに設定します。

タイマー

    func startTimer() {
        timer?.invalidate()
        timerValue = 10
        timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
            if self.timerValue > 0 {
                self.timerValue -= 1
            } else {
                self.answerQuestion(with: "")  // タイムアウト時は空の回答を送信
            }
        }

invalidateでタイマーを無効化し、古いタイマーが動作し続けることを防ぐ

timer?.invalidate()

目的: すでに設定されているタイマーがあれば、それを無効化します。これにより、古いタイマーが動作し続けることを防ぎます。
timer?: timerがnilでない場合のみinvalidateメソッドを呼び出します。
invalidate(): タイマーを無効化するメソッドです。無効化されたタイマーはもうトリガーされません。

Timer.scheduledTimer:で、新しいタイマーを設定

timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
    if self.timerValue > 0 {
        self.timerValue -= 1
    } else {
        self.answerQuestion(with: "")  // タイムアウト時は空の回答を送信
    }
}

Timer.scheduledTimer: 新しいタイマーを設定します。withTimeInterval: 1.0: タイマーが1秒ごとにトリガーされます。
repeats: true: タイマーが繰り返し動作することを指定します。
クロージャ { timer in ... }: タイマーがトリガーされるたびに実行されるコードブロックです。

タイマー処理を質問画面表示時のみ発動するように修正する


タイマーを発動する処理が、CSVファイル読み込み時に発動するロジックとなっていたので、クイズ画面表示前にタイマー処理が動いてしまっており、正しく動作しませんでした。

問題画面にいるときだけタイマー処理を発動させたい

onAppearとonDisappearを使い、ビューの表示非表示によって、タイマー制御メソッドを呼び分ける

QuizViewModel: isTimerRunningというBoolプロパティを追加し、タイマーの起動・停止を管理します。

QuizView: onAppearとonDisappearを使って、ビューが表示されるときにisTimerRunningをtrueにし、非表示になるときにfalseにしてタイマーを停止します。

これにより、QuizViewが表示されているときのみタイマーが動作し、それ以外の画面ではタイマーが停止するようにできます。

    var body: some View {
        ZStack {
            VStack {
                // 進行バー
                // 正解不正解ラベル
                // 質問文
                // 選択肢
                // スコアとタイマー
            }
            .navigationTitle("Quiz")
            .padding()
            .onAppear {
                    viewModel.isTimerRunning = true
                    viewModel.startTimer()
            }
            .onDisappear {
                    viewModel.isTimerRunning = false
                    viewModel.stopTimer()
            }

            if viewModel.showResultIcon {
                // マルバツのアニメーション
            }
        }
    }

おわりに

最後まで読んで頂きありがとうございます!

機能追加していくと、徐々にロジック量も増え、コードの確認作業や修正作業も増えてきますので、多少時間がかかってしまいますね、、(それでもだいぶ早くコーディングできましたが。)

機能改修する際には、元のロジックを考慮しながら改修していく必要があるので、ChatGPTを使ってコーディングする際には、ファンクションなどでコードを小さい単位にまとめ、全ロジック改修していくというよりは、的を絞ってコーディング依頼をしていく方が、効率よく改修できそうな気がします。

おまけ

最近、ChatGPTを使用し、色々なことを模索しています。
もしよければ、以下の記事も見て頂けると嬉しいです!


この記事が気に入ったらサポートをしてみませんか?