SwiftUIとOpenAI APIを使用したAIチャットアプリの作成

# 概要
本記事では、AppleのUIフレームワークSwiftUIとOpenAIのGPT-4を使って以下のようなAIチャットアプリを作成する方法について説明します。
![IMG_6729.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2014168/c270f33d-90c1-79e7-76a5-781fe6849cbc.jpeg)

## 対象読者
- SwiftUIの基礎ををある程度理解している人
- Swiftの開発環境がすでに構築できている人
- OpenAIのAPIを利用したIOS/Ipadアプリを作成したい人
## 開発環境
MacOS Ventura(v13.4)
Xcode(v14.3.1)
# OpenAIでの各種キー取得
以下のリンクよりOpenAIのAPIキーを取得するページにアクセスし、ログインします。
もしOpenAIのアカウントがない場合はアカウントを作成してください。

https://platform.openai.com/account/api-keys

その後以下のCreate new secret keyをクリックしAPIキーを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2014168/08206ab7-9c03-109b-32e0-e17e4c9c21f6.png)
今回はAPIキーの他にOrganization IDも必要になるためそれも取得します。
以下のページにアクセスし、Organization IDを取得してください。

https://platform.openai.com/account/org-settings
# パッケージの導入
OpenAI APIをSwiftで使用するためにOpenAIKitというパッケージを導入します。
プロジェクトをまだ作成していない場合は、作成します。
その後、File→Add Packagesを選択し右上の検索バーのところに
https://github.com/marcodotio/OpenAIKit
と入力しAdd Packageをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2014168/6d79d19a-43e3-b89c-fa8b-b38947a3ba78.png)
パッケージ依存関係の欄にOpenAIKitがされていれば完了です。

# ソースコード解説
ソースコードを実行すると以下のようになります。
![IMG_6730.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2014168/302ef66b-957c-fc47-dc02-5f8281362478.png)
今回作成したソースコードは以下の通りです。
```swift:ContentView.swift
import SwiftUI
import OpenAIKit

// チャット表示用のメインビュー
struct Chat: View {

    // 現在のチャットが完了しているかどうかを示す変数
    @State private var isCompleting: Bool = false
    
    // ユーザーが入力するテキストを保存する変数
    @State private var text: String = ""
    
    // チャットメッセージの配列
    @State private var chat: [ChatMessage] = [
        ChatMessage(role: .system, content: "あなたは、ユーザーの質問や会話に回答するロボットです。"),
        ChatMessage(role: .system, assistant:"こんにちは。何かお困りのことがあればおっしゃってください。")
    ]
    
    // チャット画面のビューレイアウト
    var body: some View {
        VStack {
            // スクロール可能なメッセージリストの表示
            ScrollView {
                VStack(alignment: .leading) {
                    ForEach(chat.indices, id: \.self) { index in
                        // 最初のメッセージ以外を表示
                        if index > 1 {
                            MessageView(message: chat[index])
                        }
                    }
                }
            }
            .padding(.top)
            // 画面をタップしたときにキーボードを閉じる
            .onTapGesture {
                UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
            }
            
            // テキスト入力フィールドと送信ボタンの表示
            HStack {
                // テキスト入力フィールド
                TextField("メッセージを入力", text: $text)
                    .disabled(isCompleting) // チャットが完了するまで入力を無効化
                    .font(.system(size: 15)) // フォントサイズを調整
                    .padding(8)
                    .padding(.horizontal, 10)
                    .background(Color.white) // 入力フィールドの背景色を白に設定
                    .overlay(
                        RoundedRectangle(cornerRadius: 20)
                            .stroke(Color.gray.opacity(0.5), lineWidth: 1.5)

                    )
                
                // 送信ボタン
                Button(action: {
                    isCompleting = true
                    // ユーザーのメッセージをチャットに追加
                    chat.append(ChatMessage(role: .user, content: text))
                    text = "" // テキストフィールドをクリア
                    
                    Task {
                        do {
                            // OpenAIの設定
                            let config = Configuration(
                                organizationId: "Organization IDを入力",
                                apiKey: "APIキーを入力"
                            )
                            let openAI = OpenAI(config)
                            let chatParameters = ChatParameters(model: "gpt-4", messages: chat)
                            
                            // チャットの生成
                            let chatCompletion = try await openAI.generateChatCompletion(
                                parameters: chatParameters
                            )
                            
                            isCompleting = false
                            // AIのレスポンスをチャットに追加
                            chat.append(ChatMessage(role: .assistant, content: chatCompletion.choices[0].message.content))
                        } catch {
                            print("ERROR DETAILS - \(error)")
                        }
                    }
                }) {
                    // 送信ボタンのデザイン
                    Image(systemName: "arrow.up.circle.fill")
                        .font(.system(size: 30))
                        .foregroundColor(self.text == "" ? Color(#colorLiteral(red: 0.75, green: 0.95, blue: 0.8, alpha: 1)) : Color(#colorLiteral(red: 0.2078431373, green: 0.7647058824, blue: 0.3450980392, alpha: 1)))
                }
                // テキストが空またはチャットが完了していない場合はボタンを無効化
                .disabled(self.text == "" || isCompleting)
            }
            .padding(.horizontal)
            .padding(.bottom, 8) // 下部のパディングを調整
        }
    }
}

// メッセージのビュー
struct MessageView: View {
    var message: ChatMessage
    
    var body: some View {
        HStack {
            if message.role.rawValue == "user" {
                Spacer()
            } else {
                // ユーザーでない場合はアバターを表示
                AvatarView(imageName: "avatar")
                    .padding(.trailing, 8)
            }
            VStack(alignment: .leading, spacing: 4) {
                // メッセージのテキストを表示
                Text(message.content)
                    .font(.system(size: 14)) // フォントサイズを調整
                    .foregroundColor(message.role.rawValue == "user" ? .white : .black)
                    .padding(10)
                    // ユーザーとAIのメッセージで背景色を変更
                    .background(message.role.rawValue == "user" ? Color(#colorLiteral(red: 0.2078431373, green: 0.7647058824, blue: 0.3450980392, alpha: 1)) : Color(#colorLiteral(red: 0.9098039216, green: 0.9098039216, blue: 0.9176470588, alpha: 1)))
                    .cornerRadius(20) // 角を丸くする
            }
            .padding(.vertical, 5)
            // ユーザーのメッセージの場合は右側にスペースを追加
            if message.role.rawValue != "user" {
                Spacer()
            }
        }
        .padding(.horizontal)
    }
}

// アバタービュー
struct AvatarView: View {
    var imageName: String
    
    var body: some View {
        VStack {
            // アバター画像を円形に表示
            Image(systemName: "person.crop.circle")
                .resizable()
                .frame(width: 30, height: 30)
                .clipShape(Circle())
            
            // AIの名前を表示
            Text("AI")
                .font(.caption) // フォントサイズを小さくするためのオプションです。
                .foregroundColor(.black) // テキストの色を黒に設定します。
        }
    }
}


// プレビュー
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        Chat()
    }
}

```
以下に各部分の説明を書きます。
## メインビュー（Chat）
ここで作成されるメインのViewはチャットの画面全体を制御します。それぞれのメッセージ（ユーザーのメッセージとAIからのレスポンス）は`chat`配列に追加され、これらは`MessageView`で表示されます。

### ScrollView
`ScrollView`を用いてメッセージリストを表示しています。`ForEach`を使用して`chat`配列内の各メッセージをループしています。そして、各メッセージについて`MessageView`を作成しています。

### TextField
ここではユーザーからの入力を受け取るテキストフィールドを設定しています。テキストフィールドは`isCompleting`の値によって無効化されます。これは、AIがレスポンスを生成している間はユーザーからの新たな入力を受け付けないようにするためです。

### Button
送信ボタンをクリックしたときに発火するアクションを定義しています。まず、`isCompleting`を`true`に設定してテキストフィールドを無効化し、次にユーザーのメッセージを`chat`配列に追加します。その後、入力フィールドをクリアし、非同期タスクを開始してAIからのレスポンスを待ちます。

この非同期タスクでは、OpenAIの設定を行い、チャットパラメータを設定しています。これにはモデル名（`gpt-4`）とchat配列が含まれます。最後に、AIからのレスポンスを取得し、それを`chat`配列に追加します。

## メッセージのビュー（MessageView）
メッセージ送信者がユーザーである場合、メッセージは右寄せになります。テキストは白色で、背景は緑色です。
メッセージ送信者がユーザーでない場合（つまり、AIからのレスポンス）、メッセージは左寄せになります。この場合、テキストは黒色で、背景は灰色です。
このビューはHStackを使用してアバターとメッセージテキストを配置し、送信者がユーザーかどうかによってメッセージの位置を調整します。これにより、ユーザーとAIからのメッセージを視覚的に区別することが可能です。
## アバタービュー（AvatarView）
非ユーザーメッセージの隣に表示されるアバター画像を表示するためのビューです。システムイメージ名`person.crop.circle`を用いてシンプルなアバターを作成しています。
システムイメージは[SF Symbols](https://developer.apple.com/sf-symbols/)にて一覧を確認することができます。
## OpenAIの設定
今回は以下の部分でAIの設定をしました。
```swift
    @State private var chat: [ChatMessage] = [
        ChatMessage(role: .system, content: "あなたは、ユーザーの質問や会話に回答するロボットです。"),
        ChatMessage(role: .assistant, content:"こんにちは。何かお困りのことがあればおっしゃってください。")
    ]
```
OpenAIのAPIにはroleが必要で`system`,`assistant`,`user`があります。
`system`が最初に性格の設定で使用する部分、`assistant`がAIの返答、`user`がユーザーの入力したものです。
今回は、メッセージの初めに`role:system`の後のcontentを入れるによりAIの性格を変え、`system`でAIの口調を決めています。

また、
```swift
let chatParameters = ChatParameters(model: "gpt-4", messages: chat)
```
にて、言語モデルを`gpt-4`に指定していますが、`gpt-3.5-turbo`にすることも可能です。
`gpt-4`は出力の精度は良いものの、ランニングコストが`gpt-3.5-turbo`の約10倍となっているため。、用途により変更する必要があります。
# まとめ
この投稿では、SwiftUIとOpenAIのGPT-4を使用してAIチャットアプリを作成する方法について説明しました。このアプリは簡単ながらも、ユーザーとAIとの対話を可能にします。UIはカスタマイズ可能なので、自身のプロジェクトに合わせて必要に応じて修正を加えることができます。

なお、今回のソースコードは基本的なものです。更なる改善や拡張（例えば、エラーハンドリングの追加、UIの改善、ユーザー体験の向上など）を行うことで、より良いアプリケーションを作成することができます。
これからSwiftUIやOpenAIを用いた開発を行う方々にとって、本記事が参考になれば幸いです。