[iOS]Swiftで Gemini API を使ってみた！

AIの代表格と言えば「ChatGPT」かと思いますが、Gooleの公式資料を見ているとSwift向けのライブラリーがあったりと意外と使いやすそうな「Gemini」なのでさっそく勉強を兼ねて使ってみようと思います。

## Geminiとは？

「Gemini」とはグーグルの生成AIサービスです。

「Google DeepMind」が開発したマルチモーダル大規模言語モデル「Gemini」
最近ではグーグルのAI「Gemini」をiPhoneに搭載？と、まだ噂レベルではありますがニュースで報じられた事もある有名な生成AIの1つです。

## ・前提条件

・Xcode 15.0 以降
・Swift アプリは、iOS 15 以降または macOS 12 以降

## ・参考

今回はGoole のチュートリアルを参考に書いていこうと思います。
https://ai.google.dev/gemini-api/docs/get-started/tutorial?lang=swift&hl=ja#generate-text-from-text-and-image-input

## ・Gemini API

Gemini API を使用するには、API キーが必要です。Google AI Studio でキーを作成します。↓
https://aistudio.google.com/app/apikey?hl=ja

:::note warn
API キーはバージョン管理システムにチェックインしないことを強くおすすめします。もう 1 つの方法は、GenerativeAI-Info.plist ファイルに保存し、.plist ファイルから API キーを読み取ることです。この .plist ファイルをアプリのルートフォルダに配置し、バージョン管理から除外してください。
:::

プロジェクトにInfo.plist(Propaty List) を追加して、右クリックし「Open As → Source Code」で開き、以下のように記述します。
_API_KEY_ 部分には「Google AI Studio」から取得したAPIキーを指定してください。

``` Swift

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>API_KEY</key>
	<string>_API_KEY_</string>
</dict>
</plist>

```

.plistからAPIを取得するコードを書きます。

```Swift
enum APIKey {
//    .plist から API キーを取得します.
    static var ApiKey: String {
        guard let filePath = Bundle.main.path(forResource: "作成した.plist名", ofType: "plist")
        else {
            fatalError("Couldn't find file ' .plist'.")
        }
        let plist = NSDictionary(contentsOfFile: filePath)
        guard let value = plist?.object(forKey: "API_KEY") as? String else {
            fatalError("Couldn't find key 'API_KEY' in '.plist'.")
        }
        if value.starts(with: "_") {
            fatalError("Follow the instructions at https://ai.google.dev/tutorials/setup to get an API key.")
        }
        return value
    }
```
　これでAPIキーの設定はOKです！

## ・APIの利用料金
利用料金については以下を参照して下さい↓
https://ai.google.dev/pricing?hl=ja

## ・SDK パッケージをプロジェクトに追加する

ここからは実装にむけて公式で推奨しているライブラリーを準備していきます。
Swift 向けの Gemini API を利用するための GoogleGenerativeAI パッケージをアプリに追加します。

追加するには [Add Packages] ダイアログで、検索バーにパッケージの URL を貼り付けAdd  Package」ボタンを押します。

https://github.com/google/generative-ai-swift

これで準備は完了なので実装に移っていきます。

## ・実装

まずAPI 呼び出しを行う前に、生成モデルを初期化する必要があります。
今回はテキストと画像入力からテキストを生成する方法を学習しようと思いますので、
Gemini 1.0 Pro Visionを使用します。

``` Swift
//生成モデルを初期化する
let visionModel = GenerativeModel(name: "gemini-pro-vision", apiKey: APIKey.ApiKey)

```

::: note
モデルバリエーションはチュートリアルから参照できます↓

https://ai.google.dev/gemini-api/docs/models/gemini?hl=ja
:::

次にGeminiのテキスト生成コードを書いていきます。今回は写真から名前を推測してもらおうと思います。enumでエラーハンドリングも追加していきます。

```Swift

class GeminiAI {

   enum GeminError: Error {
        //        画像が不正
        case invalidPrompt
        //        リクエストの失敗
        case requestFailed
        //        リクエストが不正
        case badRequest
        //        サーバーエラー
        case internalSeverError
    }
     //   モデルの生成
     //   vision(画像・テキスト入力)
    let visionModel = GenerativeModel(name: "gemini-pro-vision", apiKey: APIKey.ApiKey)
    //    写真から名前を推測する関数
    func predictName(image: UIImage) async throws -> String? {
        do {
            let prompt = "これは何ですか?"
            //        生成結果から名前を抽出
            let response = try await visionModel.generateContent(prompt, image)
            if let text = response.text {
            
                return text
            }
        } catch GeminError.invalidPrompt {
            print("画像が不正です.")
            //            画像が不正の場合の処理(例: 別の画像を取得する)
            return nil
        } catch GeminError.requestFailed {
            print("リクエストが失敗しました.")
            //            リクエストが失敗した場合の処理(例: リクエストを再実行する)
            return nil
        } catch GeminError.badRequest {
            print("リクエストが不正です.")
            //            リクエストが不正の場合の処理(例: リクエストパラメーターを確認する)
            return nil
        } catch GeminError.internalSeverError {
            print("サーバーエラーが発生しました.")
            //            サーバーエラーが発生した場合の処理(例: 時間をおいて再実行する)
            return nil
        }
        print("予期せぬエラーが発生しました.")
        
        return nil
    }
    
}
    
```
今回はあくまで勉強の一環なので、プロンプトはシンプルですが実際に使用するならば写真が何なのか？検索したいことは何なのか？などを考慮してもっと具体的に構成する必要がありそうです。

公式のプロンプトギャラリーを参考にするのもいいかと思います。↓

https://ai.google.dev/gemini-api/prompts?_gl=1*ytnso3*_ga*NDY2MzMwMjQ2LjE3MDg1OTEwNTc.*_ga_P1DBVKWT6V*MTcxNzczNDM2Mi4yLjEuMTcxNzczNDQyNC42MC4wLjU2ODk0MjUyMg..&hl=ja

今回はカメラを利用するのでAVFounddationとPhotosを使っていきます。
そのため.plistに次のような記述を追加します。

```Swift
	<key>NSCameraUsageDescription</key>
	<string>カメラを使用します</string>
	<key>NSPhotoLibraryUsageDescription</key>
	<string>写真を使用します</string>
	<key>NSCameraPortraitEffectEnabled</key>
	<false/>
```
:::note
実際に使用する際は、ユーザーへの警告表示を具体的にしないと審査でリジェクトされるので注意が必要です。
:::

カメラ処理のコードを書き、撮影後の処理の部分にGeminiAIの処理を記載して写真撮影が完了した場合に画像判定の処理を行っていこうと思います。

また、今回は撮った画像のフォトライブラリーへの保存は行わないのであくまでアクセス許可だけを記述します。

```Swift

    //    カメラを起動する
    func startCamera() {
        //        UIImagePickerControllerのインスタンスを作成
        let picker = UIImagePickerController()
        picker.sourceType = .camera
        picker.delegate = self
        //        カメラの起動モードを写真撮影モードに設定
        picker.cameraCaptureMode = .photo
        //        カメラのプレビュー画面を調整
        picker.cameraViewTransform = CGAffineTransform(scaleX: 1, y: 1)
        //        カメラ画面を表示
        present(picker, animated: true)
    }
    //    写真撮影が完了した時に呼び出される
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        //        撮影した画像を取得
        guard let image = info[.originalImage] as? UIImage else {
            return
        }
        //        プレビュー画像に設定ための処理を書く
        
        
        Task { @MainActor in
            _ = Task {
                //               撮影後の処理を書く
                let photoName = try await GeminiAI().predictName(image: image)
                //            推測結果を表示
                if let name = photoName {
                    print("名前: \(foodName)")
                    //                    名前を表示する処理を追加する.
                    
                } else {
                    print("名前の推測に失敗しました.")
                    //   　　  名前を取得できなかった場合の処理を追加する.
                    
                }
            }
        }
        //        カメラを閉じる
        picker.dismiss(animated: true)
        
    }
    //    カメラアクセス リクエストステータス
    func allowedRequestStatus() -> Bool {
        var avState = false
        var phState = false
        //        カメラへのアクセス承認状態取得
        switch AVCaptureDevice.authorizationStatus(for: .video) {
        case .authorized:
            avState = true
            break
        case .notDetermined:
            //            カメラへのアクセス承認アラート表示
            AVCaptureDevice.requestAccess(for: .video, completionHandler: { granted in
                //                メインスレッドで非同期処理
                Task {@MainActor in
                    avState = granted
                }
            })
        default:
            avState = false
            break
        }
        //        フォトライブラリーへのアクセス承認状態取得
        switch PHPhotoLibrary.authorizationStatus(for: .addOnly) {
        case .authorized:
            phState = true
            break
        case .notDetermined:
            //            フォトライブラリーへのアクセス承認申請アラート表示
            PHPhotoLibrary.requestAuthorization(for: .addOnly, handler: { status in
                //                メインスレッドで非同期処理
                Task {@MainActor in
                    if status == .authorized {
                        phState = true
                    }
                }
            })
        default:
            phState = false
            break
        }
        if avState && phState {
            return true
        } else {
            return false
        }
    }
    //    承認状態識別(許可ならSetupAVCapture呼び出し)
    func allowedStatus() -> Bool {
        var avState = false
        var phState = false
        
        if AVCaptureDevice.authorizationStatus(for: .video) == .authorized {
            avState = true
        }
        if PHPhotoLibrary.authorizationStatus(for: .addOnly) == .authorized {
            phState = true
        }
        if avState && phState {
            return true
        } else {
            return false
        }
    }
    
```
これであとはボタンや画像のプレビュー、検索結果の表示を実像してテストしてみます。

```Swift
import UIKit
import Photos
import AVFoundation
import GoogleGenerativeAI


class ViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
    
    //    プレビュー画像を表示するUIImageView
    private let previewImageView: UIImageView = {
        let imageView = UIImageView()
        imageView.contentMode = .scaleAspectFit
        return imageView
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        _ = self.allowedRequestStatus()
        addButton()
        
        //        viewにプレビューを描写
        self.view.addSubview(previewImageView)
        //        プレビュー画像の自動レイアウト制約変換(true 有効/ false 無効)
        previewImageView.translatesAutoresizingMaskIntoConstraints = false
        //        プレビュー画像の上部と親ビューの上部の余白を１００ポイントに設定(有効)
        previewImageView.topAnchor.constraint(equalTo: view.topAnchor, constant: 100).isActive = true
        //        プレビュー画像の左端と親ビューの左端の余白を５０ポイントに設定(有効)
        previewImageView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 50).isActive = true
        //        プレビュー画像の右端と親ビューの右端の余白を-50ポイントに設定(有効)
        previewImageView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant:  -50).isActive = true
        //        プレビュー画像の高さは幅と等しく設定(有効)
        previewImageView.heightAnchor.constraint(equalTo: previewImageView.widthAnchor).isActive = true
    }
    
    func showLabel(for name: String) {
        let label = UILabel()
        label.text = "\(name)"
        label.numberOfLines = 0
        label.font = .systemFont(ofSize: 18)
        label.textColor = .white
        
        let labelWidth: CGFloat = 300
        
        self.view.addSubview(label)
        
        label.translatesAutoresizingMaskIntoConstraints = false
        
        label.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        label.topAnchor.constraint(equalTo: previewImageView.bottomAnchor, constant: 50).isActive = true
        label.widthAnchor.constraint(equalToConstant: labelWidth).isActive = true
        
    }
    
    func addButton() {
        let button = UIButton()
        button.setTitle("Photo", for: .normal)
        button.contentHorizontalAlignment = .center
        button.setTitleColor(.white, for: .normal)
        button.backgroundColor = .red
        button.layer.cornerRadius = 50
        button.layer.borderWidth = 2
        button.layer.borderColor = UIColor.white.cgColor
        
        button.addTarget(self, action: #selector(addTapped), for: .touchUpInside)
        
        let buttonWidth: CGFloat = 100
        let buttonHeight: CGFloat = 100
        
        self.view.addSubview(button)
        
        button.translatesAutoresizingMaskIntoConstraints = false
        
        button.widthAnchor.constraint(equalToConstant: buttonWidth).isActive = true
        button.heightAnchor.constraint(equalToConstant: buttonHeight).isActive = true
        button.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        button.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -50).isActive = true
    }
    
    @objc func addTapped() {
        //        カメラアクセスの許可の確認
        if allowedStatus() {
            //            カメラ起動の処理を追加する.
            startCamera()
        } else {
            print("Error: No camera activation approval")
            //            カメラアクセスに関する説明を表示させる処理を追加する.
            
        }
    }
    //    カメラを起動する
    func startCamera() {
        //        UIImagePickerControllerのインスタンスを作成
        let picker = UIImagePickerController()
        picker.sourceType = .camera
        picker.delegate = self
        //        カメラの起動モードを写真撮影モードに設定
        picker.cameraCaptureMode = .photo
        //        カメラのプレビュー画面を調整
        picker.cameraViewTransform = CGAffineTransform(scaleX: 1, y: 1)
        //        カメラ画面を表示
        present(picker, animated: true)
    }
    //    写真撮影が完了した時に呼び出される
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        //        撮影した画像を取得
        guard let image = info[.originalImage] as? UIImage else {
            return
        }
        //        プレビュー画像に設定
        previewImageView.image = image
        
        Task { @MainActor in
            _ = Task {
                
                let photoName = try await GeminiAI().predictName(image: image)
                //            推測結果を表示
                if let name = photoName {
                    print("料理名: \(name)")
                    //                    名前を表示する処理を追加する.
                    showLabel(for: name)
                } else {
                    print("料理名の推測に失敗しました.")
                    //                    名前を取得できなかった場合の処理を追加する.
                    
                }
            }
        }
        //        カメラを閉じる
        picker.dismiss(animated: true)
        
    }
    //    カメラアクセス リクエストステータス
    func allowedRequestStatus() -> Bool {
        var avState = false
        var phState = false
        //        カメラへのアクセス承認状態取得
        switch AVCaptureDevice.authorizationStatus(for: .video) {
        case .authorized:
            avState = true
            break
        case .notDetermined:
            //            カメラへのアクセス承認アラート表示
            AVCaptureDevice.requestAccess(for: .video, completionHandler: { granted in
                //                メインスレッドで非同期処理
                Task {@MainActor in
                    avState = granted
                }
            })
        default:
            avState = false
            break
        }
        //        フォトライブラリーへのアクセス承認状態取得
        switch PHPhotoLibrary.authorizationStatus(for: .addOnly) {
        case .authorized:
            phState = true
            break
        case .notDetermined:
            //            フォトライブラリーへのアクセス承認申請アラート表示
            PHPhotoLibrary.requestAuthorization(for: .addOnly, handler: { status in
                //                メインスレッドで非同期処理
                Task {@MainActor in
                    if status == .authorized {
                        phState = true
                    }
                }
            })
        default:
            phState = false
            break
        }
        if avState && phState {
            return true
        } else {
            return false
        }
    }
    //    承認状態識別(許可ならSetupAVCapture呼び出し)
    func allowedStatus() -> Bool {
        var avState = false
        var phState = false
        
        if AVCaptureDevice.authorizationStatus(for: .video) == .authorized {
            avState = true
        }
        if PHPhotoLibrary.authorizationStatus(for: .addOnly) == .authorized {
            phState = true
        }
        if avState && phState {
            return true
        } else {
            return false
        }
    }
    
}
```
## ・サンプル動画

![IMG_3609.GIF](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3808574/0c2efb95-9798-8900-6b37-f29fdeb73a95.gif)




## おわりに
ライブラリーが用意してあるので比較的簡単に少ないコードで扱うことができました！
さらに知識を深めて自身のアプリに活かしてみようかなと思います。