見出し画像

体調記録くん音声入力機能開発記録

序文

まとまったお休みが取れたので体調記録くんに機能追加することにしました。せっかくなので今流行りのAIを利用して開発してみます。利用するAIはGitHub CopilotとOpenAI chatGPTです。また飛躍的に賢くなったらしいGPT-4を使えるように$20/月を課金しました。

まとめ

ChatGPT

  • 機能のアイデア出しや開発ステップを考える段階ではかなり役に立ちました

  • ただ実際はできないことをできると言うことがあり気をつけないと振り回されると感じました

  • 今回の用途ではGPT-3.5とGPT-4の差はあまり感じられませんでした

GitHub Copilot

  • これから書こうと思っているものに近いコードをサジェストしてくれかなり効率が上がると感じました

  • サジェストされたものを受け入れてエラーを直してたりない実装を追加するといった流れでの開発になりました

開発の見通し

  • ショートカットアプリで使えるアクションを追加することはできました

  • リマインダーの様にパラメータまで含めたフレーズでSiriから体調を登録する機能は実現方法がわからず開発見通しがたちませんでした

開発機能のアイデア出し

早速chatGPTに機能アイデアを5つ挙げてもらいました

質問

入力
5段階の体調記録と簡単なメモを記録するiPhoneアプリ体調記録くんを作っています。入力のしやすさを売りにしたいのですが、新しい機能のアイデアを5つ教えてください

回答

  1. 音声入力機能: アプリ内で音声入力をサポートし、ユーザーが手間をかけずに簡単に体調やメモを記録できるようにします。これにより、入力のしやすさが向上し、ユーザーが素早く情報を記録できます。

  2. カスタマイズ可能なウィジェット: ユーザーが自分のホーム画面に配置できるウィジェットを提供し、簡単に今日の体調を記録できるようにします。ウィジェットは、5段階の体調スケールを表示し、ユーザーがタップするだけで記録できるように設計されます。

  3. AI機能を活用した体調分析: アプリ内でAIを活用し、ユーザーの過去の体調データを分析し、パターンや異常を検出します。これにより、ユーザーが自分の体調の変化に気づきやすくなり、適切な対策を講じることができます。

  4. リマインダー機能: ユーザーが毎日一貫して体調を記録することができるように、リマインダー機能を実装します。これにより、ユーザーはアプリを開くことを忘れず、毎日の体調記録を継続できます。

  5. シェア機能: ユーザーが自分の体調データやメモを簡単に友人や家族と共有できるように、シェア機能を追加します。これにより、ユーザーは他の人と情報を共有し、サポートやアドバイスを得ることができます。

判断

「シンプルでとことん入力しやすい」をコンセプトにしている体調記録くんとしては1の音声入力機能か2のウィジェット機能のどちらかが良さそうです。今回は利用の様子が明確にイメージできた音声入力機能を実装してみたいと思います。「Hey, Siri. 体調記録にとても良いと記録して!」と頼めるようにしたいです。

音声入力機能の実装方法の調査

実装方法のイメージが何もないのでざっくりと聞いてみる

質問

体調記録くんというiPhoneアプリを開発しています。Siri経由でデータを入力する機能をつけたいと考えているのですが、どのように実現できますか?

回答

「体調記録くん」というiPhoneアプリにSiri経由でデータを入力する機能を追加するには、SiriKitを利用してアプリを開発することができます。以下に、SiriKitを使って機能を実現するための基本的な手順を示します。

  1. SiriKitのドキュメントを確認:
    まず、SiriKitのドキュメントを確認し、どのような機能が利用可能か理解しておくことが重要です。
    ドキュメント:https://developer.apple.com/documentation/sirikit

  2. SiriKitの機能を有効化:
    プロジェクトの設定で、SiriKitの機能を有効にしてください。これにより、Siriとアプリが連携できるようになります。

  3. Intentsファイルを作成:
    Intentsファイルを作成し、アプリがサポートするカスタムインテントを定義します。このファイルでは、ユーザーのアクションや、アプリが提供するサービスに関連するインテントを定義できます。

  4. IntentHandlerを実装:
    IntentHandlerプロトコルを採用したクラスを実装し、Siriからのリクエストを処理できるようにします。このクラスでは、Siriからのリクエストに応じて、アプリが提供するサービスを実行するメソッドを実装する必要があります。

  5. Intent Extensionをプロジェクトに追加:
    プロジェクトにIntent Extensionを追加し、IntentHandlerを使ってSiriからのリクエストを処理できるようにします。

  6. NSUserActivityを使用してアプリの状態を共有:
    NSUserActivityを使用して、アプリの状態をSiriと共有し、データ入力などのタスクを実行できるようにします。

  7. テストとデバッグ:
    アプリをテストし、Siri経由でのデータ入力が正しく機能することを確認してください。

これらの手順に従って、Siri経由でデータを入力する機能を「体調記録くん」アプリに実装できます。具体的なコードや実装方法については、Appleの公式ドキュメントやチュートリアルを参照してください。

回答を受けて

SiriKitのドキュメントを読めとのことなので、要約を20行で作ってもらいました。
----- 以下要約 -----
SiriKitは、iOSやwatchOSアプリがSiriやMapsと連携できるようにするフレームワークです。これにより、ユーザーはSiriを通じてアプリの機能を使うことができます。

SiriKitを使ってアプリに対話機能を追加するには、次のステップが必要です。

  1. インテント定義ファイルの作成

  2. インテントハンドラーの実装

  3. インテント登録

  4. インテントUIの作成 (オプション)

SiriKitは、アプリ内で音声操作を行うための機能も提供しています。これには、Speech Recognizerを使います。

また、Siri Shortcutsを使って、ユーザーがSiriにカスタムフレーズを登録し、アプリのアクションを呼び出すことができます。Shortcutsは、アプリ内でNSUserActivityやカスタムインテントを使って登録します。

SiriKitは、複数のドメインとインテントタイプをサポートしています。主なドメインには、メッセージ、リスト、ノート、タスクなどがあります。これらのドメインには、標準的なインテントが用意されており、アプリで利用できます。

しかし、特定のアプリケーションに特化した独自のインテントを使用することも可能です。その場合、カスタムインテントを定義する必要があります。

最後に、テストとデバッグを行い、Siriとアプリが正しく連携することを確認してください。
----- 要約終わり -----

具体的な手順も質問しましたが、それっぽい説明だけどXCodeには実際にないタグやセクションの入力が必要というような回答が多く共通して繰り返し出てきた正しいであろう下記の概要だけを参考にすることにしました。

  • SiriKitを利用する

  • インテントを作成する

  • 生成されたIntentHandlingプロトコルを実装する

  • Intents App Extension を追加してhandlerメソッドを実装する

  • ローカライゼーションでSiriに与えるフレーズの多言語化ができる

  • {}で変数名を括ることで多言語化しつつ変数を扱える

加えて検索してショートカット追加ボタンの実装とローカライゼーションの参考になりそうなページを見つけました。
https://phrase.com/blog/posts/siri-shortcuts-localization-tutorial/

質問(要約依頼)

https://phrase.com/blog/posts/siri-shortcuts-localization-tutorial/ このURLの記事をポイントをまとめた日本語の文章にしてください

回答

このURLの記事では、Siriショートカットのローカリゼーションについてのチュートリアルが紹介されています。以下は、その要点をまとめた日本語の文章です。

  1. Siriショートカットは、iOSアプリを操作できる音声コマンドで、多言語対応が可能です。

  2. ショートカットをローカリゼーションすることで、多言語での使用が容易になり、ユーザーエクスペリエンスが向上します。

  3. Xcodeプロジェクトに新しい言語を追加することで、ローカリゼーションを開始できます。

  4. ローカリゼーションに必要なファイルは、Localizable.stringsとInfoPlist.stringsです。

  5. Siriショートカットのタイトルや説明文をローカリゼーションするには、Localizable.stringsファイルに対応する言語ごとの翻訳を追加します。

  6. InfoPlist.stringsファイルでは、アプリの機能を表すキーワードやフレーズをローカリゼーションします。

  7. ローカリゼーションが完了したら、テストを行い、各言語での動作を確認します。

このチュートリアルでは、Siriショートカットのローカリゼーション方法について分かりやすく説明されており、開発者が簡単に多言語対応を実現できるようになっています。

ショートカット経由での入力機能の実装

調査した実装方法に従って実装していきます。
SiriKitの有効化、Intents定義ファイルの追加までは前述のチュートリアルのおかげもあって難なく実施できました。

質問

Intent定義ファイルにCustom Classという項目を見つけたのですがここで定義されているprotocolを実装すれば良いのでしょうか?

回答

はい、その通りです。Intent定義ファイル(.intentdefinition)にあるCustom Classに関連付けられたプロトコルを実装することで、SiriKitが提供する機能を拡張したり、独自のロジックを追加することができます。

たとえば、あるタスクを追加するための独自インテントを作成した場合、以下の手順でCustom Classとプロトコルを実装できます。

  1. .intentdefinitionファイルを開き、独自インテントを選択します。

  2. 右側のインスペクターパネルで「Class Generation」セクションを展開し、Custom Classを指定します。例えば、「AddTaskIntentHandler」という名前でクラスを作成することができます。

  3. 指定したCustom Classをプロジェクト内で新しく作成し、プロトコルを実装します。Swiftの場合、`INIntentHandler`プロトコルを実装する必要があります。

例:

import Intents

class AddTaskIntentHandler: NSObject, AddTaskIntentHandling {
    func handle(intent: AddTaskIntent, completion: @escaping (AddTaskIntentResponse) -> Void) {
        // インテントを処理するロジックを実装します
    }
    
    // 必要に応じて、他のプロトコルメソッドを実装します
}
  1. `IntentHandler`クラスで、SiriKitに新しく作成したCustom Classを提供するように指示します。`IntentHandler`クラスは、通常、`IntentsAppExtension`ターゲット内にある`IntentHandler.swift`ファイルにあります。

例:

import Intents

class IntentHandler: INExtension {
    override func handler(for intent: INIntent) -> Any? {
        if intent is AddTaskIntent {
            return AddTaskIntentHandler()
        }
        
        return nil
    }
}

これで、独自のインテントが実行されると、指定したCustom Classのインスタンスが作成され、プロトコルに従って実装されたメソッドが呼び出されます。ここで、アプリ固有のロジックを実装して、SiriKitと連携することができます。

実装

RecordLatestConditionIndentHandlingというプロトコルが自動生成されていたので実装する。
class RecordLatestConditionIntentHandlerまで入力したところでGithub copilotのサジェストで下記コードが出てきました。

class RecordLatestConditionIntentHandler: NSObject, RecordLatestConditionIntentHandling {
    func handle(intent: RecordLatestConditionIntent, completion: @escaping (RecordLatestConditionIntentResponse) -> Void) {
        let response = RecordLatestConditionIntentResponse(code: .success, userActivity: nil)
        completion(response)
    }
}

コード案を採用するとRecordLatestConditionIntentHandlingのメソッドを定義できていないとエラーになるのでresolveConditionを追加しました。
生成されたメソッドの前半に当日の体調記録を取得して、書き換えて保存する処理を追加していきます。

質問

IntentHandlingプロトコルのresolveXXX(XXXはパラメータ名)を実装したいのですが、この中では何をすることを期待されていますか?

回答

`IntentHandling`プロトコルの `resolveXXX` メソッドは、インテントが受け取ったパラメータを解決(検証や確認)するために使用されます。このメソッドは、インテントが受け取ったパラメータが不完全、不正確、または明確でない場合に、ユーザーに対して追加情報や選択肢を提示することができます。

`resolveXXX`メソッドでは以下のことを行うことが期待されています:

  1. パラメータが有効かどうかをチェックします。例えば、入力された日付が過去のものであるか、入力されたテキストが空でないかなどです。

  2. パラメータが不完全または曖昧な場合、ユーザーに対して選択肢を提供します。たとえば、あいまいな場所名が入力された場合、複数の場所を選択肢として提示して、ユーザーが正確な場所を選択できるようにします。

  3. パラメータが正確で完全であることを確認したら、`.success` を返して解決を完了します。これにより、インテントが次のステップ(`confirm` や `handle` メソッド)に進むことができます。

例として、タスク追加インテントでタスクのタイトルを解決する場合の実装は以下のようになります:

func resolveTitle(for intent: AddTaskIntent, with completion: @escaping (INStringResolutionResult) -> Void) {
    if let title = intent.title, !title.isEmpty {
        completion(INStringResolutionResult.success(with: title))
    } else {
        completion(INStringResolutionResult.needsValue())
    }
}

この例では、タスクのタイトルが空でないことを確認し、空でなければ成功を返します。もしタイトルが空の場合、Siriがユーザーにタイトルを求めるように指示するために、`INStringResolutionResult.needsValue()` を返します。

`resolveXXX`メソッドは、必要に応じてインテントの他のパラメータに対しても実装することができます。解決プロセスが正しく機能するように、適切な解決結果を返すことが重要です。

実装

パラメーターはEnumなので検証の必要がないため値が存在すれば無条件でsuccessを返すように実装しました。値が存在しない場合はneedsValueを指定します。
続けてプロジェクトを選択してターゲットの追加ボタンからIntent Extensionを選択して追加します。
自動生成されたIntentHandler.swiftを開いてhandlerを実装します。回答にあったようにintentが一致したらHandlerを返す実装をします。
Cannot findとエラーになるためHandlerをIntentExtensionに移動しました。合わせてTarget Membershipも少し調整しました。

なんとかショートカット経由で指定した体調を入れられるようになったけれど体調記録くんを開いても入力した体調が反映されません。デバッガで見てみるとAppとExtensionでRealmを共有できずそれぞれで持っている様子でした。

質問

AppとExtensionでRealmを使ってデータを共有したいのですがどうすれば良いですか?

回答

AppとExtensionでRealmを使ってデータを共有する方法は、App Groupを設定して共有コンテナを使用することです。これにより、アプリケーションとエクステンション間でデータを簡単に共有できます。

手順は以下の通りです。

  1. App Groupを設定する

    • Xcodeで、アプリのターゲットを選択し、「Capabilities」タブを開きます。

    • 「App Groups」セクションを展開し、「+」ボタンをクリックして新しいApp Groupを作成します。これは、`group.<your_bundle_identifier>.realm`のような形式になります。

  2. 同じApp Groupをエクステンションに追加する

    • エクステンションのターゲットを選択し、「Capabilities」タブを開きます。

    • 「App Groups」セクションを展開し、先ほど追加したApp Groupにチェックを入れます。

  3. Realmの設定を変更する

    • アプリケーションとエクステンションの両方で、以下のようにRealmの設定を変更して、共有コンテナにあるファイルを使用するようにします。

import RealmSwift

func setupRealm() -> Realm? {
    let fileURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.<your_bundle_identifier>.realm")!.appendingPathComponent("default.realm")

    let config = Realm.Configuration(
        fileURL: fileURL,
        schemaVersion: 1,
        migrationBlock: { migration, oldSchemaVersion in
            if oldSchemaVersion < 1 {
                // 何もしない。データベーススキーマの変更がなければ、このコードブロックは空にしておきます。
            }
        }
    )

    // 全てのRealmインスタンスに適用するデフォルト設定を更新
    Realm.Configuration.defaultConfiguration = config

    do {
        return try Realm()
    } catch let error {
        print("Error initializing Realm: \(error)")
        return nil
    }
}
  1. アプリケーションとエクステンションでRealmを使用する

    • 各場所で`setupRealm()`関数を呼び出し、返されたRealmインスタンスを使用してデータの読み書きを行います。

これで、アプリケーションとエクステンション間でRealmデータベースを共有できるようになります。データベースの操作は、通常通りRealmのAPIを使用して行ってください。

質問

XCodeのバージョンが14.2と古いせいか「Capabilities」タグはなくて「Signing & Capabilities」タグしかなく「AppGroup」セクションもありません。14.2での情報はありますか?

回答

すみません、「Signing & Capabilities」タブが正しいです。Xcode 14.2でも、App Groupの設定方法は基本的に同じです。手順を以下のように修正します。

  1. App Groupを設定する

    • Xcodeで、アプリのターゲットを選択し、「Signing & Capabilities」タブを開きます。

    • 左上の「+ Capability」ボタンをクリックし、「App Groups」を選択して追加します。

    • 「App Groups」セクションが表示されるので、「+」ボタンをクリックして新しいApp Groupを作成します。これは、`group.<your_bundle_identifier>.realm`のような形式になります。

  2. 同じApp Groupをエクステンションに追加する

    • エクステンションのターゲットを選択し、「Signing & Capabilities」タブを開きます。

    • 左上の「+ Capability」ボタンをクリックし、「App Groups」を選択して追加します。

    • 「App Groups」セクションが表示されるので、先ほど追加したApp Groupにチェックを入れます。

その後、Realmの設定を変更し、アプリケーションとエクステンション間でデータの読み書きを行う方法は、前回の回答で説明した通りです。

実装

App Groupをそれぞれのターゲットに追加してRealmのConfigurationでfileURLを設定したところ、ショートカット経由で入力した値がアプリで開いた画面にも反映されるようになった。
ただし、既存のデータの置かれていた場所からデータの置き場所を変えたせいで既存データが見れなくなってしまった。追加の質問で既存データを新しい置き場のデータに移行する方法を教えてもらって実装しました。

行き詰まる

「{}で変数名を括ることで多言語化しつつ変数を扱える」の設定の仕方が調べても分からず行き詰まってしまいました。
多言語かはできるものの変数名を指定しても何も変わらず実現できませんでした。
調べても何も見つけられず、ChatGPTを問い詰めても「実際には、インテントの定義時にパラメータを含むカスタムフレーズを設定する方法は存在しません。」と言われてしまいます。
標準アプリだけれどリマインダーで出来ているので実現方法はありそうなのですが。。公式アプリだけ特別に許可されている何かなのでしょうか。

この記事が気に入ったらサポートをしてみませんか?