Apple visonOSを使用したAIチャットアプリの作成

# 概要
本記事では、SwiftUIとOpenAIKitを使用してvisonOS用のチャットボットのユーザーインターフェースを実装する方法を解説しています。
以下の画像は実行した時の様子です
![Simulator Screenshot - Apple Vision Pro - 2023-06-22 at 13.54.30.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2014168/12135280-1a4a-18f4-3d6d-edf1697b5645.jpeg)
![vions2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2014168/418f92ed-b173-e84b-2c92-457eed5be05c.jpeg)

こちらの記事のプログラムを少し変更しています。

https://qiita.com/xyzmiku/items/711f0ac642a542b6719e

## 開発環境
MacOS Ventura(v13.4)
Xcode-bata(v15.0 beta 2 (15A5161b))

# 開発環境の構築
まずは以下のApple公式サイトにアクセスし右上のDownload SDKをクリックします。

https://developer.apple.com/visionos/

ログインをすると以下のような画面になると思うのでvisionOS 1 betaにチェックを入れてダウンロードをしてください。 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2014168/c5c4b2d2-cebf-2dc9-31cc-e80ea6a54d34.png)

ダウンロードをしたXcode-betaを開き、Create New Project→visonOSタブを選択→Nextを選択
そのあとはIOSアプリなどと同じようにお好みで設定してください。

# パッケージ・各種キーの取得
以下の記事を参考に取得してください。

https://qiita.com/xyzmiku/items/711f0ac642a542b6719e

# ソースコード解説

今回作成したソースコードは以下の通りです。
前述した記事とほぼ同じなので、変更点のみ解説します。
```swift:ContentView.swift
import SwiftUI
import OpenAIKit
import RealityKit
import RealityKitContent

// チャットメッセージの表示を扱うView
struct Chat: View {
    
    // チャットの送信状態とメッセージを管理するState変数
    @State private var isCompleting: Bool = false
    @State private var text: String = ""
    @State private var chat: [ChatMessage] = [
        ChatMessage(role: .system, content: "あなたはユーザーの質問に答える優秀なアシスタントです。"),
        ChatMessage(role: .assistant, content: "こんにちは。質問があればお聞かせください。")
    ]
    
    // 没入型スペースを開くための環境変数
    @Environment(\.openImmersiveSpace) var openImmersiveSpace
    @Environment(\.dismissImmersiveSpace) var dismissImmersiveSpace
    @State var showImmersiveSpace = false
    
    var body: some View {
        NavigationSplitView {
            List {
                Text("Chat")
            }
            .navigationTitle("Sidebar")
        } detail: {
            VStack {
                createChatArea()
                createMessageInputArea()
            }
            .navigationTitle("AIChat")
            .padding()
        }
        .onChange(of: showImmersiveSpace) { _, newValue in
            handleImmersiveSpaceChange(newValue)
        }
    }
    
    // チャットエリアの作成
    private func createChatArea() -> some View {
        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)
        }
    }
    
    // メッセージ入力エリアの作成
    private func createMessageInputArea() -> some View {
        HStack {
            createTextField()
            createSendButton()
        }
        .padding(.horizontal)
        .padding(.bottom, 8)
    }
    
    // テキストフィールドの作成
    private func createTextField() -> some View {
        TextField("Enter message", text: $text)
            .disabled(isCompleting)
            .font(.system(size: 15))
            .padding(8)
            .background(
                RoundedRectangle(cornerRadius: 20)
                    .stroke(Color.gray.opacity(0.5), lineWidth: 1.5)
            )
    }
    
    // 送信ボタンの作成
    private func createSendButton() -> some View {
        Button(action: {
            sendMessage()
        }) {
            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)
    }
    
    // メッセージの送信
    private func sendMessage() {
        isCompleting = true
        chat.append(ChatMessage(role: .user, content: text))
        text = ""
        
        Task {
            do {
                let config = Configuration(
                    organizationId: "あなたのorganizationId",
                    apiKey: "あなたのapiKey"
                )
                let openAI = OpenAI(config)
                let chatParameters = ChatParameters(model: "gpt-4", messages: chat)
                
                let chatCompletion = try await openAI.generateChatCompletion(parameters: chatParameters)
                
                isCompleting = false
                chat.append(ChatMessage(role: .assistant, content: chatCompletion.choices[0].message.content))
            } catch {
                print("ERROR DETAILS - \(error)")
            }
        }
    }
    
    // 没入型スペースの変更をハンドリング
    private func handleImmersiveSpaceChange(_ newValue: Bool) {
        Task {
            if newValue {
                await openImmersiveSpace(id: "ImmersiveSpace")
            } else {
                await dismissImmersiveSpace()
            }
        }
    }
}

// メッセージのView
struct MessageView: View {
    var message: ChatMessage
    
    var body: some View {
        HStack {
            if message.role == .user {
                Spacer()
            } else {
                AvatarView()
                    .padding(.trailing, 8)
            }
            createMessageView()
            if message.role != .user {
                Spacer()
            }
        }
        .padding(.horizontal)
    }
    
    // メッセージのビューを作成
    private func createMessageView() -> some View {
        VStack(alignment: .leading, spacing: 4) {
            Text(message.content)
                .font(.system(size: 14))
                .foregroundColor(message.role == .user ? .white : .black)
                .padding(10)
                .background(message.role == .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)
    }
}

// アバターのView
struct AvatarView: View {
    
    var body: some View {
        VStack {
            // アバター画像を円形に表示
            Image(systemName: "person.crop.circle")
                .resizable()
                .frame(width: 30, height: 30)
                .clipShape(Circle())
        }
        
        Text("AI")
            .font(.caption)
            .foregroundColor(.white)
    }
}


// プレビュー
#Preview {
    Chat()
}
```

## 変更点
### 新しい環境変数と没入型スペース
新しいバージョンでは、沈没型スペースの開閉を管理するための新しい環境変数`openImmersiveSpace`と `dismissImmersiveSpace`が導入されています。これらは、`Immersive Space`を開閉するための関数を提供します。また、`showImmersiveSpace`という新しい`State`変数が追加され、これが変化したときに沈没型スペースの開閉を管理します。

### NavigationSplitViewの導入
新しいバージョンでは、UIの主要部分は`NavigationSplitView`で構築されています。これは、主な視覚的変更の一つで、チャットとサイドバーの両方を表示できるようにするためのものです。

### MessageViewの微調整
新しいバージョンの`MessageView`では、`message`の色付けとスペーシングが微調整されています。

### create系の関数導入
新しいバージョンでは、コードの構造が少し変更され、各部分を生成するための関数が導入されています。具体的には、`createChatArea`, `createMessageInputArea`, `createTextField`, `createSendButton`, `createMessageView`といった関数が追加されています。これにより、コードが整理され、各部分の生成が明確になります。

### AvatarViewの変更
新しいバージョンの`AvatarView`では、"AI"というテキストの色が白色に変更されています。また、画像名のパラメータが削除されています。
# まとめ
この記事ではvisonOSで動作する、SwiftUIとOpenAIのGPT-4を用いたAIチャットアプリの作り方を紹介しました。このアプリは、簡潔ながらユーザーとAIが会話を交わすことを可能にしています。また、UIは完全にカスタマイズ可能で、プロジェクトの必要に応じて修正を加えられるようになっています。

本ソースコードは基本的なものであり、更なる改良や拡張（エラーハンドリングの追加、UIの改善、ユーザーエクスペリエンスの向上など）を通じて、より洗練されたアプリケーションを作成することができます。

SwiftUIやOpenAIを使用した開発をこれから始める方々にとって、この記事が一助となることを願っています。