見出し画像

ChatGPTを使用し、1時間でiPhoneアプリを作ってみた(swiftUI)

今回はChatGPTでiPhoneアプリを作っていこうと思います!
結構前にstory boredを使用し、少しだけアプリを作ってみたことがありますが、swiftUIは使用したことがなかったため、とりあえずVStackとナビゲーション遷移の仕方を勉強して、早速ChatGPTでコーティングしてみることにしました!

プロンプト

クイズ画面を表示したい
クイズ問題はCSVファイルを読み込んで、クイズを表示する
クイズの内容は英単語を答えるクイズとする
クイズは4択とする
問題数は10問とする
CSVファイルの問題はサンプルの問題をいくつか作成してください

ChatGPTの回答

まず、クイズの問題を含むCSVファイルを作成します。このファイルには、各行に問題、正解、選択肢1、選択肢2、選択肢3、選択肢4を含むようにします。以下にサンプルのCSVファイルの内容を示します。

サンプルCSVファイル (quiz_questions.csv)

question,correct,option1,option2,option3,option4
What is the English word for "犬"?,dog,cat,mouse,bird
What is the English word for "猫"?,cat,dog,mouse,bird
What is the English word for "鳥"?,bird,cat,mouse,dog
What is the English word for "ネズミ"?,mouse,cat,bird,dog
What is the English word for "馬"?,horse,cow,sheep,goat
What is the English word for "牛"?,cow,horse,sheep,goat
What is the English word for "羊"?,sheep,horse,cow,goat
What is the English word for "山羊"?,goat,horse,cow,sheep
What is the English word for "ライオン"?,lion,tiger,leopard,cheetah
What is the English word for "トラ"?,tiger,lion,leopard,cheetah

次に、SwiftUIを使用してこのCSVファイルを読み込み、クイズ画面を表示するコードを作成します。

CSVファイルの読み込みとクイズ表示のコード

import SwiftUI
import Foundation

struct QuizQuestion: Identifiable {
    let id = UUID()
    let question: String
    let correct: String
    let options: [String]
}

class QuizViewModel: ObservableObject {
    @Published var questions: [QuizQuestion] = []
    @Published var currentQuestionIndex = 0
    @Published var score = 0
    
    init() {
        loadCSV()
    }
    
    func loadCSV() {
        guard let path = Bundle.main.path(forResource: "quiz_questions", ofType: "csv") else {
            print("CSV file not found")
            return
        }
        
        do {
            let data = try String(contentsOfFile: path)
            let rows = data.components(separatedBy: "\n")
            for row in rows.dropFirst() {
                let columns = row.components(separatedBy: ",")
                if columns.count == 6 {
                    let question = columns[0]
                    let correct = columns[1]
                    let options = Array(columns[2...5])
                    let quizQuestion = QuizQuestion(question: question, correct: correct, options: options.shuffled())
                    questions.append(quizQuestion)
                }
            }
            questions.shuffle()
        } catch {
            print("Error reading CSV file: \(error)")
        }
    }
    
    func answerQuestion(with answer: String) {
        if questions[currentQuestionIndex].correct == answer {
            score += 1
        }
        currentQuestionIndex += 1
    }
    
    func restartQuiz() {
        score = 0
        currentQuestionIndex = 0
        questions.shuffle()
    }
}

struct QuizView: View {
    @ObservedObject var viewModel = QuizViewModel()
    
    var body: some View {
        NavigationView {
            VStack {
                if viewModel.currentQuestionIndex < viewModel.questions.count {
                    let question = viewModel.questions[viewModel.currentQuestionIndex]
                    Text(question.question)
                        .font(.title)
                        .padding()
                    ForEach(question.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)
                    }
                } else {
                    Text("Quiz Completed!")
                        .font(.largeTitle)
                        .padding()
                    Text("Your score: \(viewModel.score)/\(viewModel.questions.count)")
                        .font(.title)
                        .padding()
                    Button(action: {
                        viewModel.restartQuiz()
                    }) {
                        Text("Restart Quiz")
                            .padding()
                            .background(Color.green)
                            .foregroundColor(.white)
                            .cornerRadius(10)
                    }
                }
            }
            .navigationTitle("Quiz")
            .padding()
        }
    }
}

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()
    }
}

動作確認

多少バグもありましたが、1時間程度で画面表示することができました!
(csvのカラム数は5個だが、6個のロジックとなっていたり。。)

                if columns.count == 6 { // ロジック不備。CSVのカラム数は5個
                    let question = columns[0]
                    let correct = columns[1]
                    let options = Array(columns[2...5]) // ロジック不備。
                    let quizQuestion = QuizQuestion(question: question, correct: correct, options: options.shuffled())
                    questions.append(quizQuestion)
                }




ChatGPTにコードの内容を教えてもらう

無事画面を表示することができましたので、次にロジックの各ステップについてChatGPTに教えてもらおうと思います。

質問データを格納する構造体

struct QuizQuestion: Identifiable {
    let id = UUID() // 一意のIDを生成
    let question: String // クイズの質問
    let correct: String // 正しい答え
    let options: [String] // 選択肢のリスト
}

Identifiableプロトコル

Identifiable プロトコルに準拠することで、この構造体のインスタンスは一意のIDを持ち、SwiftUIのリストやForEachビューで使用する際に特定できるようになります。このプロトコルには、idというプロパティを持つ必要があります。

Identifiableプロトコルを付けないと、ForEachやListでエラーとなる

Identifiableプロトコルを付けない場合、SwiftUIのForEachやListなどのビューでこの構造体を使用する際に、一意に識別できるプロパティを持たないため、エラーが発生します。Identifiableプロトコルは、リストアイテムやその他のコレクション内の要素を一意に識別するために必要です。

質問データを管理するクラス

変数の変更を検知しビューを自動更新(ObservableObjectプロトコル)

ObservableObject プロトコルに準拠することで、このクラスはSwiftUIのビューに対してデータの変更を通知できます。ビューはこの変更を監視し、自動的に更新されます。これにより、モデルとビューの間のデータバインディングが可能になります。

class QuizViewModel: ObservableObject {
    @Published var questions: [QuizQuestion] = []
    @Published var currentQuestionIndex = 0
    @Published var score = 0
    
    init() {
        loadCSV()
    }
    
    func loadCSV() {
        guard let path = Bundle.main.path(forResource: "quiz_questions", ofType: "csv") else {
            print("CSV file not found")
            return
        }
        
        do {
            let data = try String(contentsOfFile: path)
            let rows = data.components(separatedBy: "\n")
            for row in rows.dropFirst() {
                let columns = row.components(separatedBy: ",")
                if columns.count == 6 {
                    let question = columns[0]
                    let correct = columns[1]
                    let options = Array(columns[2...5])
                    let quizQuestion = QuizQuestion(question: question, correct: correct, options: options.shuffled())
                    questions.append(quizQuestion)
                }
            }
            questions.shuffle()
        } catch {
            print("Error reading CSV file: \(error)")
        }
    }
    
    func answerQuestion(with answer: String) {
        if questions[currentQuestionIndex].correct == answer {
            score += 1
        }
        currentQuestionIndex += 1
    }
    
    func restartQuiz() {
        score = 0
        currentQuestionIndex = 0
        questions.shuffle()
    }
}

@Published var questions: [QuizQuestion] = []

クイズの質問を格納する配列です。@Published属性を付けることで、このプロパティが変更されたときにビューが自動的に更新されるようになります。

ObservableObjectプロトコルと@Publishedプロパティをつけないと、ビューが自動的に更新されない

ObservableObjectプロトコルと@Publishedプロパティを使用しない場合、次のような問題が発生します。
ビューの更新がされない: データモデルの変更がビューに反映されず、ユーザーインターフェイスが最新のデータを表示しません。
データバインディングが機能しない: SwiftUIのデータバインディング機能が動作しないため、手動でビューを更新する必要があります。

ObservableObjectプロトコルと@Publishedプロパティを使う理由

これらを使用することで、データの変更を自動的にビューに反映させることができ、リアクティブなユーザーインターフェイスを簡単に構築できます。例えば、以下のコードのように、ObservableObjectと@Publishedを使うことで、データ変更がビューに自動的に反映されます。

CSVファイルの読み込み処理

CSVのファイルパスの取得

guard let path = Bundle.main.path(forResource: "quiz_questions", ofType: "csv") else {
     print("CSV file not found")
     return
}

このコードスニペットは、アプリケーションのバンドル内に含まれるリソースファイル(この場合は"quiz_questions.csv")のパスを取得するために使用されます。以下に、このコードの詳細な説明を行います。

1. Bundle.main.path(forResource:ofType:)

Bundle.mainは、アプリケーションのメインバンドルを指します。バンドルとは、アプリケーションに含まれるリソース(画像、データファイル、ストーリーボードなど)をまとめたディレクトリです。path(forResource:ofType:)メソッドは、バンドル内の指定されたリソースファイルのパスを取得します。forResource: リソースファイルの名前(拡張子を含まない)。
ofType: リソースファイルの拡張子。


2. guard文

guard文は、条件が満たされない場合に特定のブロックを実行し、関数やメソッドから早期にリターンするために使用されます。この場合、CSVファイルのパスを取得できなかった場合にエラーメッセージを表示し、メソッドからリターンします。

3. pathのオプショナル型

path(forResource:ofType:)メソッドは、リソースが見つからない場合にnilを返すため、pathはオプショナル型になります。guard let構文を使用して、pathがnilでないことを確認しています。

CSVファイルの読み込み処理

        do {
            let data = try String(contentsOfFile: path)
            let rows = data.components(separatedBy: "\n")
            for row in rows.dropFirst() {
                print("loop")
                let columns = row.components(separatedBy: ",")
                
                print(columns.count)
                if columns.count == 5 {
                    
                    let question = columns[0]
                    let correct = columns[1]
                    let options = Array(columns[1...4])
                    let quizQuestion = QuizQuestion(question: question, correct: correct, options: options.shuffled())
                    questions.append(quizQuestion)
                }
            }
            questions.shuffle()
        } catch {
            print("Error reading CSV file: \(error)")
        }

このコードは、CSVファイルを読み込んでその内容を処理し、クイズの質問と選択肢を格納するためのものです。各行を分割して質問を作成し、それを配列に追加します。詳細は以下の通りです。

1. do { ... } catch { ... }ブロック

このブロックは、エラーハンドリングのためのもので、ファイルの読み込み中にエラーが発生した場合に対応します。

2. let data = try String(contentsOfFile: path)
内容: 指定されたファイルパスからファイルの内容を文字列として読み込みます。
try: 例外が発生する可能性があるため、tryキーワードを使用します。
エラーハンドリング: 読み込みに失敗した場合、catchブロックが実行されます。

3. let rows = data.components(separatedBy: "\n")
内容: 読み込んだファイルの内容を行ごとに分割し、各行をrows配列に格納します。
分割基準: 改行文字(\n)。

4. for row in rows.dropFirst()
内容: 最初の行(通常はヘッダ行)を除外し、各データ行を処理します。
dropFirst(): 最初の要素を除外します。

5. print("loop")
内容: 各行の処理が開始されるたびに「loop」とコンソールに出力します。デバッグ用の出力です。

6. let columns = row.components(separatedBy: ",")
内容: 各行をカンマ(,)で分割し、各フィールドをcolumns配列に格納します。

7. print(columns.count)
内容: 各行の列数をコンソールに出力します。デバッグ用の出力です。

8. if columns.count == 5
内容: 各行に5つのフィールドがあることを確認します。もしそうでない場合、その行は無視されます。

9. let question = columns[0]
内容: 最初のフィールドを質問として取得します。

10. let correct = columns[1]
内容: 2番目のフィールドを正しい答えとして取得します。

11. let options = Array(columns[1...4])
内容: 2番目から5番目のフィールドを選択肢として取得します(インデックス範囲が間違っているため修正が必要です。正しくはcolumns[2...4])。

12. let quizQuestion = QuizQuestion(question: question, correct: correct, options: options.shuffled())
内容: QuizQuestion構造体の新しいインスタンスを作成します。選択肢はシャッフルされます。

13. questions.append(quizQuestion)
内容: 作成したQuizQuestionインスタンスをquestions配列に追加します。

14. questions.shuffle()
内容: 全ての質問をランダムに並べ替えます。

15. catch { print("Error reading CSV file: \(error)") }内容: ファイルの読み込み中にエラーが発生した場合にエラーメッセージをコンソールに出力します。

配列の要素をシャッフルする

questions.shuffle()

questions.shuffle() は、Swiftの標準ライブラリに含まれるメソッドで、配列内の要素をランダムに並べ替えるために使用されます。このメソッドを使用すると、配列の要素の順序がランダムに変更されます。

質問文を表示するビュー

                    let question = viewModel.questions[viewModel.currentQuestionIndex]
                    Text(question.question)
                        .font(.title)
                        .padding()
                    ForEach(question.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)
                    }

各種コードの説明その1

このコードは、SwiftUIを使用してクイズの質問と選択肢を表示するビューの一部です。現在の質問を表示し、その質問に対する複数の選択肢をボタンとして表示します。それぞれの選択肢ボタンをクリックすると、選択した答えが処理されます。

各部分の説明

1. let question = viewModel.questions[viewModel.currentQuestionIndex]
内容: viewModelのquestions配列から現在の質問を取得します。currentQuestionIndexは現在表示すべき質問のインデックスを示しています。


2. Text(question.question)
内容: 現在の質問テキストを表示します。
修飾子:
.font(.title): テキストのフォントサイズをタイトルサイズに設定します。
.padding(): テキスト周囲にパディング(余白)を追加します。

ループで質問の選択肢を4つ表示する

3. ForEach(question.options, id: \.self) { option in ... }内容: 現在の質問に対する複数の選択肢を反復処理し、それぞれの選択肢をボタンとして表示します。
引数:
question.options: 選択肢の配列。
id: \.self: 各選択肢が一意であることを保証するための識別子。文字列自体を識別子として使用します。

ForEach内で使用されるid: \.selfは、配列の各要素を一意に識別するために使用(struct QuizQuestion: Identifiableのidとは関係なし)

上記のid: \.selfは、struct QuizQuestion: Identifiableで定義されているidとは直
接関係がありません。以下にそれぞれのidの役割と関係を説明します。

役割: ForEach内で使用されるid: \.selfは、question.options配列の各要素(選択肢)自体が一意であると仮定し、その要素を一意に識別するために使用されます。selfは配列の各要素をそのまま識別子として使用することを意味します。
適用範囲: question.options配列の各要素(選択肢)に適用されます。各選択肢が文字列である場合、文字列そのものが識別子として使用されます。

選択肢(ボタン)を押下すると、ボタン押下時用の処理が発火

4. Button(action: { ... }) { ... }
内容: 選択肢ごとにボタンを作成します。
引数:
action: ボタンが押されたときに実行されるクロージャ。ここではviewModel.answerQuestion(with: option)が呼び出され、選択した答えが処理されます。
label: ボタンのラベルを設定します。ここでは選択肢のテキストが表示されます。

各種コードの説明その2

5. Text(option)内容: ボタンのラベルとして、選択肢のテキストを表示します。

6. .frame(maxWidth: .infinity)内容: ボタンの幅を最大限に広げます。親ビューの幅いっぱいに広がります。

7. .padding()内容: ボタンの内側にパディングを追加します。ボタンのテキストとボタンの境界との間に余白を作ります。

8. .background(Color.blue)内容: ボタンの背景色を青に設定します。

9. .foregroundColor(.white)内容: ボタンのテキスト色を白に設定します。

10. .cornerRadius(10)内容: ボタンの角を10ポイントの半径で丸くします。

11. .padding(.horizontal)内容: ボタンの周囲に水平方向のパディングを追加します。左右に余白を作ります。

withは、Swiftのメソッド宣言において引数ラベルを指定するためのキーワード

    func answerQuestion(with answer: String) {
        if questions[currentQuestionIndex].correct == answer {
            score += 1
        }
        currentQuestionIndex += 1
    }

引数ラベルは、メソッドや関数を呼び出すときに使用する名前です。引数ラベルはメソッドや関数の定義で指定され、引数ラベルと引数の名前を異なるものにすることができます。これにより、メソッドや関数をより読みやすく、理解しやすくすることができます。

引数ラベルと引数名を使い分ける理由

引数ラベルと引数名を使い分ける理由は、メソッドの読みやすさと可読性を向上させるためです。

引数ラベル: メソッドを呼び出すときに、引数の意味を明確にするために使用されます。
引数名: メソッドの内部で引数にアクセスするために使用されます。

おわりに

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

ChatGPTに動くサンプルコードを教えてもらえ、わからないロジックはすぐにChatGPTが答えてくれるので、効率よく学習することができました!
今後もChatGPTを使用し、色々なiPhoneを作ってみようと思います。

おまけ

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





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