🍎

ChatGPT-4にアプリの雑な仕様からSwiftUIのアプリを出力させてみた💡

2023/03/27に公開

背景

今話題中の話題のChatGPTがコードも実装してくれると聞き、
「雑に書いた仕様からSwiftUIのアプリを作成」というのに、チャレンジしてみました。
今回は、ChatGPT-4に、自分で用意したクイズアプリの仕様書を読み込ませて、クイズアプリをSwiftUIで実装してもらいました。

結論

「つっこみどころはあるが、仕様通りに動くものは作れやがる!すごい!」

▼アプリのプレビュー

  • タイトル画面から始まり、スタートボタンでクイズ画面に遷移をしてクイズに答える。
  • 全てクイズを答えると結果画面に遷移する。
  • 結果画面では正答率と回答を閲覧することができる。結果画面からタイトルに戻ることができる。

▼デモ
https://github.com/yosei-yamagishi/SpecialViewList/tree/main/SpecialViewList/Component/View/QuizAppByChatGPT

▼画面

タイトル画面 クイズ画面 クイズ正解 クイズ失敗 最後のクイズ クイズ結果画面

やったこと

  • 「ChatGPT Plus」という有料プランに入り、ChatGPT-4を使用
  • プロンプト(入力)は、クイズアプリの仕様
  • アウトプット(出力)は、SwiftUIのクイズアプリの実装

プロンプト

プロンプトは以下になります。

あなたは、優秀なiOSエンジニアでかつ優秀なUI/UXのデザイナーです。
クイズアプリを以下に記述する、クイズのアプリの仕様と必ず含める条件を満たすものを具体的に実装

# 必ず含める条件
* クイズアプリは、SwiftUIで実装
* クイズアプリの実装には、@mainファイルも含む
* クイズアプリで使用する具体的な日本語のクイズを5問作成し、クイズアプリにそのクイズを組み込んで実装
* クイズアプリのすべての画面で画面の縦幅に収まるように調整
* 最近のSwiftUIのアプリでよく使われているアーキテクチャを採用
* デザインは、Appleのデザインリソースのデザインテンプレートを参考
* 実装したコードはエラーが出力されないか確認し出力(特にIndex out of rangeに注意)

# クイズのアプリの仕様
* クイズアプリのタイトルは「Quiz de Show!!」
* クイズアプリの画面遷移は、タイトル画面から始まり、クイズ画面に切り替わり、クイズを回答すると次々とクイズ画面が表示され、5問目が終了したときにクイズ結果画面が出力
* クイズの選択肢は4つ
* タイトル画面でスタートボタンを押すと、クイズ画面に遷移してクイズが出題
* クイズ画面では問題に対して4つの選択項目があり、タップすると正解/不正解が表示
* クイズを5問答えるとクイズ結果画面に遷移して、クイズの正解率、クイズの問題と回答と正解がリストで表示される。
* クイズ結果画面では、もう一度タイトル画面に戻る

## クイズアプリの画面仕様
1. タイトル画面
* アプリ名表示
* スタートボタンでクイズボタンに遷移

2. クイズ画面
* ナビゲーションタイトルにはクイズが何問目か表示
* 日本語のクイズの問題(見やすい位置に配置に大きく表示)
* クイズの選択肢1、2のボタンが横並びで表示
* クイズの選択肢3、4のボタンが横並びで表示
* クイズに正解した場合、「正解」と表示され、正解の選択肢が強調される
* クイズに不正解した場合、「間違い!」と表示され、間違った選択肢と正解の選択肢が、別々のデザインで強調される
* クイズ回答後に次へボタンが出現する
* 次へボタンを押すと次のクイズ画面が表示
* 最後の問題の場合はクイズ結果画面が表示
* 問題文と選択肢のテキストが適切な大きさで表示

3. クイズ結果画面
* クイズの回答の正解率
* 出題されたクイズの結果(回答したクイズの問題とクイズの正解とクイズの回答)
* タイトルに戻るボタン
* タイトルに戻るを押すとクイズがリセットされてタイトル画面に遷移

実装内容

出力結果は以下になります。
※Xcodeで新規作成したプロジェクトに、コピペで貼れば動きます。

import SwiftUI

struct Quiz {
    let question: String
    let choices: [String]
    let correctAnswer: String
}

@main
struct QuizApp: App {
    var body: some Scene {
        WindowGroup {
            TitleView()
        }
    }
}

struct TitleView: View {
    var body: some View {
        NavigationView {
            VStack {
                Text("Quiz de Show!!")
                    .font(.largeTitle)
                    .padding()
                
                NavigationLink(destination: QuizView()) {
                    Text("スタート")
                        .font(.title2)
                        .padding()
                        .background(Color.blue)
                        .foregroundColor(.white)
                        .cornerRadius(10)
                }
            }
        }
    }
}

struct QuizView: View {
    @State private var showResults = false
    @State private var selectedAnswer: String?
    @State private var questionIndex = 0
    @State private var selectedAnswers: [String] = []
    
    let quizzes = [
        Quiz(question: "日本の首都はどこ?", choices: ["東京", "大阪", "福岡", "名古屋"], correctAnswer: "東京"),
        Quiz(question: "富士山は何県にある?", choices: ["山梨県", "静岡県", "長野県", "新潟県"], correctAnswer: "静岡県"),
        Quiz(question: "日本の最高峰は?", choices: ["御嶽山", "北アルプス", "富士山", "八ヶ岳"], correctAnswer: "富士山"),
        Quiz(question: "日本の最も古い歴史書は?", choices: ["日本書紀", "古事記", "竹取物語", "源氏物語"], correctAnswer: "古事記"),
        Quiz(question: "「鳥取砂丘」はどこの都道府県にある?", choices: ["島根県", "鳥取県", "岡山県", "広島県"], correctAnswer: "鳥取県")
    ]
    
    var body: some View {
        VStack {
            Text("問題 \(questionIndex + 1)")
                .font(.title)
                .padding()
            
            Text(quizzes[questionIndex].question)
                .font(.title2)
                .padding()
            
            VStack {
                ForEach(0..<2) { i in
                    AnswerButton(
                        text: quizzes[questionIndex].choices[i],
                        selectedAnswer: $selectedAnswer,
                        correctAnswer: quizzes[questionIndex].correctAnswer
                    )
                }
                
                ForEach(2..<4) { i in
                    AnswerButton(
                        text: quizzes[questionIndex].choices[i],
                        selectedAnswer: $selectedAnswer,
                        correctAnswer: quizzes[questionIndex].correctAnswer
                    )
                }
            }
            
            if let selectedAnswer = selectedAnswer {
                if selectedAnswer == quizzes[questionIndex].correctAnswer {
                    Text("正解!")
                        .font(.title)
                        .foregroundColor(.green)
                } else {
                    Text("間違い!")
                        .font(.title)
                        .foregroundColor(.red)
                }
            }
            
            if showResults {
                NavigationLink(destination: ResultsView(quizList: quizzes, selectedAnswers: selectedAnswers)) {
                    Text("結果を表示")
                        .font(.title2)
                        .padding()
                        .background(Color.green)
                        .foregroundColor(.white)
                        .cornerRadius(10)
                }
            } else {
                Button(action: {
                    selectedAnswers.append(selectedAnswer ?? "")
                    if questionIndex < quizzes.count - 1 {
                        questionIndex += 1
                        selectedAnswer = nil
                    } else {
                        showResults = true
                    }
                }) {
                    Text("次へ")
                        .font(.title2)
                        .padding()
                        .background(Color.blue)
                        .foregroundColor(.white)
                        .cornerRadius(10)
                }
                .opacity(selectedAnswer == nil ? 0 : 1)
            }
        }
    }
}
struct AnswerButton: View {
    let text: String
    @Binding var selectedAnswer: String?
    let correctAnswer: String
    
    var body: some View {
        Button(action: {
            selectedAnswer = text
        }) {
            Text(text)
                .font(.title2)
                .padding()
                .background(selectedAnswer == text ? (text == correctAnswer ? Color.green : Color.red) : Color.blue)
                .foregroundColor(.white)
                .cornerRadius(10)
        }
        .disabled(selectedAnswer != nil)
        .padding()
    }
}

struct ResultsView: View {
    let quizList: [Quiz]
    let selectedAnswers: [String]
    
    private var correctAnswers: Int {
        selectedAnswers.indices.filter { selectedAnswers[$0] == quizList[$0].correctAnswer }.count
    }
    
    private var scorePercentage: Int {
        Int(Double(correctAnswers) / Double(quizList.count) * 100)
    }
    
    var body: some View {
        VStack {
            Text("正解率: \(scorePercentage)%")
                .font(.largeTitle)
                .padding()
            
            List {
                ForEach(0..<quizList.count) { i in
                    HStack {
                        VStack(alignment: .leading) {
                            Text(quizList[i].question)
                                .font(.headline)
                            
                            Text("正解: \(quizList[i].correctAnswer)")
                                .font(.subheadline)
                                .foregroundColor(.green)
                        }
                        
                        Spacer()
                        
                        Text(selectedAnswers[i] == quizList[i].correctAnswer ? "正解" : "不正解")
                            .font(.headline)
                            .foregroundColor(selectedAnswers[i] == quizList[i].correctAnswer ? .green : .red)
                    }
                }
            }
            
            NavigationLink(destination: TitleView()) {
                Text("タイトルに戻る")
                    .font(.title2)
                    .padding()
                    .background(Color.blue)
                    .foregroundColor(.white)
                    .cornerRadius(10)
            }
            .padding(.bottom)
        }
    }
}

やってみたが失敗したこと

  • アーキテクチャを指定してみたりしたが、複雑にすると一発の出力ではなかなかエラーがでてビルドができなかった
  • デザインをもう少し凝ったものにしようと思ってゲームぽくとか指定してみたが、そんなにUI/UXは変わらなかった
  • ChatGPT-3.5は、実装してくれるときと実装してくれないときがあり、実装しても仕様を満たしていない実装(クイズが1問目だけ回答して次に遷移しないとか)が出力された

Discussion