8
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Posted at

概要

本記事では、SwiftUIとOpenAIKitを使用してvisonOS用のチャットボットのユーザーインターフェースを実装する方法を解説しています。
以下の画像は実行した時の様子です
Simulator Screenshot - Apple Vision Pro - 2023-06-22 at 13.54.30.jpg
vions2.jpg

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

開発環境

MacOS Ventura(v13.4)
Xcode-bata(v15.0 beta 2 (15A5161b))

開発環境の構築

まずは以下のApple公式サイトにアクセスし右上のDownload SDKをクリックします。

ログインをすると以下のような画面になると思うのでvisionOS 1 betaにチェックを入れてダウンロードをしてください。
image.png

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

パッケージ・各種キーの取得

以下の記事を参考に取得してください。

ソースコード解説

今回作成したソースコードは以下の通りです。
前述した記事とほぼ同じなので、変更点のみ解説します。

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

変更点

新しい環境変数と没入型スペース

新しいバージョンでは、沈没型スペースの開閉を管理するための新しい環境変数openImmersiveSpacedismissImmersiveSpaceが導入されています。これらは、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を使用した開発をこれから始める方々にとって、この記事が一助となることを願っています。

8
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?