ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
Appインテントの詳細
App Intentsフレームワークで、Appを見つけやすくし、Appのエンゲージメントを向上させる方法をご確認ください。Swiftフレームワークの強力な機能をはじめ、AppインテントとSiriKitインテントの違い、Appの機能をシステムに公開する方法について解説します。また、豊富なAppショートカット体験をもたらすエンティティやクエリを作成する方法も紹介します。 Appインテントのさらなる詳細については、WWDC22の「AppインテントによるAppショートカットの実装」および「Appショートカットのデザイン」をご確認ください。
リソース
関連ビデオ
WWDC23
Tech Talks
WWDC22
WWDC21
-
ダウンロード
♪ 明るい音楽 ヒップホップミュージック♪ ♪ みなさん こんにちは Michael Gorbachです Shortcuts Engineeringに 所属しています この度は Appの機能を システムに公開するための 新しいフレームワーク Appインテントについて 詳しく見ていきます こちらが今回見ていく プランです 導入部分を簡潔に 説明した後に インテントや パラメータについて話し エンティティの定義について 話します 強力な検索機能や フィルタリング機能を構築し インテントがどのように ユーザーとインタラクション するのか確認します 最後にAppインテントのアーキテクチャおよび ライフサイクルについて説明します 初めから見ていきましょう iOS 10では SiriKit Intents フレームワークを導入し メッセージ ワークアウト 支払いなどのSiriのドメインに Appの機能を 対応させられるようにしました 今回はAppインテントという 新しいフレームワーク を導入します 3つの重要な コンポーネントがあります インテントは Appに組み込まれた アクションのことで システム全体で 使用することができます インテントはエンティティを使用して Appのコンセプトを表現します Appショートカットは インテントをラップすることで 自動的かつ インテントを発見しやすくします Appインテントによって Appの機能を より多くの場所で 利用できるようになり どのように顧客に利益を 提供できるのか話していきます Appショートカットを使うと 最初に何も設定しなくても 誰でも音声で Siriを通して Appの機能を 使うことができます 同じ実装で Spotlightで Appを検索した際や Appが提案された際に インテントが表示されます これによりみなさんの仕事が 前面の中央へと現れます Appインテントを使用して 集中モードフィルタを構築できます 顧客は特別な集中モード用に Appをカスタマイズできます 例えば カレンダーAppを 設定して 仕事中に仕事のカレンダーのみを 表示することができます 集中モードフィルタを導入する 方法について こちらセッションで 詳しく確認してください Appショートカットを使用すると インテントがショートカットAppに マニュアルでの追加の必要なく 自動的に表示されます ショートカットにAppの アクションを統合することは 顧客にとって非常に 価値あることです というのも システムを通じて 様々な場所から ショートカットを呼び出すことで Appの機能を活用できるためです ホーム画面のシングルタップや macOSのメニューバーから それ以外の様々な場所からも ショートカットを実行できます オートメーションによって 自動的に実行される ショートカットを 設定することもできます ショートカットを サポートすることで Appがショートカット エコシステム全体とつながり Appleや他のデベロッパの さまざまな Appの力を借りて パワーと能力を 倍増させることができます ショートカットは 複数のappのアクション を組み合わせることが できるため ユーザーが何もしなくても まったく新しい機能 や性能を生み出すことが できるからです みなさんのアクションを 他とうまく連動させ このエコシステムにシームレスに 適合させる方法を確認するには 私どものデザインに関する セッションを確認してください Appインテントを 構築する目的は 開発する楽しみを 感じてもらうことでした Appインテントは 簡潔に使用できます 単純なインテントを 書くには 数行のコードだけですが APIはより深く カスタマイズ可能な アクションにもスケールします Resultビルダー プロパティラッパー プロトコル指向プログラミング ジェネリックスを活用し Swiftに全面的に 取り組んできました これらのAPIは 最先端の言語機能なしには 機能できません Appインテントの 導入は簡単です プロダクトやターゲットの再設計や フレームワークの作成は 必要ありません Extensionも必要とせず App内ですぐに対応できます Appインテントのコードは メンテナンス面の特徴もあります SwiftUIなどと同様 Appインテントはコードを 基盤となる信頼できる ソースとして使用するため 別のエディタや定義ファイルの 必要性を回避することができます これにより迅速な構築と 採用における反復が可能になり すべてが一箇所に 集約されるため メンテナンスも 簡素化されます それでは 新しい フレームワークの 主要な構成要素である インテントから
これらの新しい APIを見ていきます Appインテント 略してインテントは Appがシステムに公開する 単一で分離された 機能の単位です 例えば インテントには 新しい カレンダーイベントを作成 特定の画面を開く 注文する などがあります インテントは ショートカットの実行 やSiriでの検索など ユーザーのリクエストに 応じて実行することも 集中モードフィルタや ショートカットオートメーションを 使って自動的に実行 することも可能です インテントを実行する際に 結果を返すか エラーを投げるか のどちらかになります インテントには次の3つの 重要な要素が含まれます ローカライズされた タイトルなどの インテントに関する メタデータや情報 インテントが 実行される際に 使用できる入力 となるパラメータ そして インテントが 実行される際に 実際の作業を行う performメソッドです 本日は読書Appから 話していきます 私は大の読書家のため 読んだ本 読みたい本 現在読んでいる本 を記録したい と思っています Shelfという Appの個別タブに 各カテゴリが表示されます ユーザーがいつも 現在読んでいる本の Shelfを訪れるので Appインテントを公開して より素早く より便利に これを開けるようにします AppIntent プロトコルに準拠した Swiftの構造体を 定義することで OpenCurrentlyReading インテントをここに作成します 実装が必要なメソッドは performメソッドだけです Appには タブを開く Navigatorがすでにあるため 私にとっての インテントの実装は 数行のコードのみです Navigatorはメインスレッド を想定しているため performメソッドに@MainActorの アノテーションを付けます インテントには タイトルも必要です 本日紹介する 他の文字列のように キーをstringsファイルに 追加すると 自動的に ローカライズされます ここまでがAppインテントのために 必要な基本的な実装です コードで定義されたので ショートカットエディタに 自動的に表示されます そこで ユーザーが ショートカットに追加できます このインテントを公開するだけで 大きな効果が得られます 顧客がこのインテントを ショートカットにすれば システムの様々な場所から インテントが 使用可能になるからです この新しいインテントを簡単に 使え 見つけられるように Appショートカットの サポートを追加します ほんの少しのコードで Spotlightや ショートカットAppに インテントを自動的に 表示させることができ Siriに対する音声経由で このインテントを使うための フレーズも定義する ことができます すべての詳細につきましては 「Appインテントによる Appショートカットの実装」 セッションをご確認ください インテントを公開して 現在読んでいる本の Shelfを開きます 次に これを一般化して どのShelfにも開けるように パラメータを追加しましょう Shelfを表す enumがあります パラメータとして 使用するために AppEnumプロトコルに 対応させる必要があります AppEnumには StringのRawデータが 必要であるため まず最初に追加します 各enumのケースには ローカライズ可能で 人が認識できるタイトルを 付ける必要があります コンパイラはビルド時に このコードを読むため これらは辞書リテラル として提供される必要があります 最後に typeDisplayName を追加します これは この列挙型全体に対する ユーザから見える ローカライズ可能な名前です 「Shelf」を 使用します インテントでは 各パラメータは @Parameterプロパティ ラッパーで宣言され タイトルなどの パラメータに対する 情報で初期化されます ここでは performメソッドで読み込む 新しいshelfパラメータ を定義します パラメータは 数値 文字列 ファイルなどの他にも エンティティや 列挙型など これらすべての型に 対応しています このインテントをショートカット エディタで表示するとこのようになります Shelfパラメータは テーブルに示されています ParameterSummary APIを使うことで UIをより合理的にし ショートカットにうまく 適合できます パラメータサマリは 「Open 」 のように エディタ内でインテントと そのパラメータを表す文章表現です ショートカットのベストな 結果のためにも 作成する すべてのインテントで パラメータサマリを 常に提供することをお勧めします どのパラメータを下に表示 または非表示にするか 定義することができます これらのAPIには WhenとOtherwise API またはSwitch Case Default APIを使用して インテントの任意のパラメータ の実際の値に基づいて サマリを変化させるなど 優れた機能があります パラメータサマリ を追加するために このstaticプロパティ を実装します ここでは文字列 「Open」を返し shelfパラメータを 補間します Open Shelfを 機能させるために 最後に必要なことは このように インテントを実行すると 読書Appが 開くようにすることです Appの起動は staticプロパティ openAppWhenRun で管理します デフォルトはfalseで ほとんどの インテントに合致しています しかし このようにUIで 何かを開くインテントの場合 この値をtrueに 設定する必要があります ちょうど今Shelfを開くための インテントを作成しました Shelfのセットを修復したので こちらは非常に簡潔なりました Bookを開くインテントを 構築したい場合 そのセットが固定ではなく 動的なものであればどうでしょうか そのために エンティティが必要になります エンティティとは Appインテントに Appが公開するコンセプトです メモAppのノートや 写真Appの写真やアルバムなど 値が動的または ユーザー定義の場合は 列挙型の代わりにエンティティを 使用する必要があります エンティティのインスタンスを 提供するために Appでクエリを実装し インテントから結果として エンティティを返すことができます Appで本を開くというインテントを 作成することから始めます ショートカットエディタでは このように表示されます Bookパラメータを タップすると Appが提供する 提案された エンティティのセットを含む 本を選択するピッカーが 利用できます ピッカーの上部にある この検索フィールドを使えば ライブラリにあるどんな 本でも探すことができます インテント自体を 構築する前に 本のエンティティと それに対応する クエリを作成する 必要があります エンティティは少なくとも 識別子 ディスプレイ表現 エンティティ型の名前 3つを含みます エンティティを追加するには 構造体を AppEntityプロトコルと 適合することから始めます ここではBookEntityの 新しい構造体を定義しますが モデルから既存の型を 適合させることもできます Identifiable プロトコルにエンティティを 適合させることで 識別子を提供します Appインテントはこの識別子 を使用してAppとシステムの 他の部分との間で送信される エンティティを参照します 識別子は 顧客が作成した ショートカットに保存される 可能性があるため 安定かつ永続的で あることが必要です ディスプレイ表現は このエンティティをユーザーに 表示するために使用されます これは本のタイトルなどの テキストの文字列を 単純なものにできます サブタイトルや画像を 提供することも可能です typeDisplayNameは エンティティ型を 表す人が読みやすい 文字列です この例では 「Book」です ここで本のエンティティを 実行するために クエリを追加する 必要があります クエリはAppから エンティティを取得するための インターフェイスを システムに提供します クエリではいくつかの方法で エンティティを検索できます すべてのクエリは 識別子に基づいた エンティティを検索できる 必要があります 文字列クエリで検索 をサポートします 後ほど より柔軟性の高い プロパティクエリを ご案内します すべてのクエリでユーザーが リストから選択できるように エンティティの提案を 提供することもできます すべてのエンティティはクエリと 関連付けられる必要があるため システムがそのエンティティの インスタンスを検索できるようになります EntityQuery プロトコルに準拠した Swiftの構造体を作成して クエリを提供します 基本的なクエリには 必須メソッドが1つあり それを実装することで 識別子の配列を取得して エンティティを解決します モデルデータベースに アクセスして その識別子に一致する本を 探すことでこれを実装しました クエリをエンティティに フックする必要があります BookEntity型の defaultQuery staticプロパティを実装し BookQueryの インスタンスを返すことで これを実現しています ユーザーが本を選ぶ際に その識別子が ショートカットに保存されます ショートカットを 実行すると Appインテントは この識別子をクエリに渡し BookEntity インスタンスを取得します BookEntity型が AppEntityプロトコル に適合することで OpenBookインテントの パラメータとして使用できます performメソッドは Navigatorを介して 本へと遷移させます
本のピッカーを サポートするために クエリは提案された 結果も提供する必要があります そのためには読書Appに 追加されたすべての本を返す もう1つのメソッドをクエリに 実装する必要があります ショートカットは これらの結果で ピッカーを埋めます ショートカットUIには上部に 検索フィールドがあります Appはたくさんの本の エンティティを持つ可能性があるため 本当はAppのプロセスで 直接データベースに対して 検索を実行する必要があります StringQuery APIを使うと それができます StringQueryサブプロトコル を採用することで 文字列を指定して結果を返す entity (matching string:) という もう一つのメソッドを 実装できるようになりました ここでは 本のタイトルに対して 単純に大文字小文字を 区別しない マッチングで 実装していますが 例えば著者名や シリーズ名から検索するなど よりファンシーな ことも可能です 膨大な数と少ない数の お気に入りの本のリストがある場合 suggestedEntitiesで お気に入りだけを返し entities (matching string:) で ユーザーが長いリスト 全体を検索できます Appで本を開く方法を 公開し その過程で本のエンティティと 本のクエリを構築しました 同じエンティティとクエリを使用して インテントをもっと作成できます 次のタスクは インテントを構築して ライブラリに本を 追加することです 顧客は共有シートの ショートカットを使用して オンラインを閲覧しながら 素早く本を追加したり 画面を見ることなく HomePodのSiriに 本の追加を指示したり することが可能です このようにUIを表示することなく モデルを直接操作する インテントを構築することで ユーザーに力を与えることができます 本のタイトルと 著者のオプション名を パラメータとして取る AddBookインテント の実装があります どの友人がその本を勧めたか を記録するための メモもオプションで 用意されています performメソッドでは async/awaitを使用した APIコールで本を調べて ライブラリに追加します 一致するものが見つからない場合は エラーを投げます このエラーをローカライズ するためにエラー型を CustomLocalizedStringResource Convertibleプロトコルに適合します このプロパティから ローカライズされた 文字列のキーを返して そのキーをstringsファイルに追加します このAdd Bookインテントは Siriやウィジェットなどのために そのままでも 驚くほど便利です 他のインテントと組み合わせる ことができる場合 さらに柔軟性が高まります 少しの作業をすることで AddBookインテントを 先ほど構築した OpenBookインテント と組み合わせ 一方からもう一方に 結果を渡すことができます そのためにAddBook インテントの結果の 一部として 値を返すようにします performメソッド の戻り値の型に 新しいプロトコルが 追加されたこと に注目してください ユーザーはこのインテントの 結果値をパラメータとして 本のエンティティを受け取る 他のエンティティに接続ができます AddBookインテントと OpenBookインテントが 自然に組み合わさるため 本を追加してすぐに ライブラリで開く ショートカットを 作成することができます インテントから 結果を返して それをAppで開くことは よくあることです Appインテントには openIntentという 表現方法が 組み込まれています openIntentを追加すると 顧客はショートカットに 「Open When Run」という新しい スイッチを取得することになります スイッチをオフにする場合 このインテントを ショートカットの一部として バックグラウンドで中断することなく 使用できるようになります スイッチを 入れたままにした場合 新しく追加された本がすぐに 読書Appで開きます openIntentの採用は OpenBookインテントの インスタンスを作成し 結果の一部として 返すと簡単です このインテントを 実行する際に Open When Runスイッチが オンになっていると AddBookインテントが 終了した後に OpenBookインテントが 自動的に実行されます エンティティおよびクエリを使用して たくさんのことが実行できます 次のAPIセットによって SiriKit Intentsフレームワークでは これまでに実現できなかったような 強力な機能を Appインテントで活用できます エンティティの情報を より多く公開し 顧客がその情報を基に 検索やフィルタリングする 方法について説明します これまで 本のエンティティに 必要な基本的要件 をすべて追加しています 本をもっと深く ショートカットに 組み込むには 本についてもう少し 公開する必要がありそうです エンティティはプロパティ をサポートしており ユーザーに公開したい エンティティの追加情報 を保持します 今回の場合は 本の著者 出版日 読書日 推薦者を追加し ショートカットでそれらのプロパティ を使うことができます BookEntityに プロパティを追加するには @Propertyというプロパティ ラッパーを使用します プロパティはパラメータ と同じ型に対応しており それぞれローカライズ されたタイトルを取ります これらの新しい プロパティにより 顧客は本のエンティティを 処理する際に ショートカットの マジック変数を使用して それぞれ新しい情報を引き出すこと ができるようになりました 先ほどのAddBook インテントを使用する際 新たに追加された 本の著者や出版日を ショートカットで 使用することができます
プロパティとクエリ を組み合わせる際に Appは自動的にこの柔軟な 述語エディタUIを備えた 非常に強力な検索と フィルタアクションを ショートカットに対し 得ることができます これで顧客は読書日 タイトル 著者名などに基づいて 本を検索し フィルタリング できるようになりました 例えば Delia Owensの本を すべて探すのは非常に 簡単なことです Sort byやLimit オプションを使えば Delia Owensの 最近出版された 3冊の本を探すなど さらに高度なクエリに 対応することができます 顧客はこの構成要素を使って コレクションの中で最も著名な 3名の作家を見つけるなど 非常に優れた機能が 利用できます これらすべてを 可能にするために プロパティクエリという 別の種類のクエリを 採用する必要があります プロパティクエリは文字列や 識別子に基づくのではなく エンティティ内のプロパティに 基づいてエンティティを検索します プロパティクエリの実装には 3つのステップがあります まず最初にクエリ プロパティを宣言して そのプロパティを使用して エンティティを検索する方法を 指定します 次にソートオプション を追加し クエリ結果のソート方法 を定義します 最後にentities(matching:) を実装して 検索を実行します クエリプロパティは Appインテントが このクエリに関連する エンティティで検索できる あらゆる方法を表示します エンティティのプロパティと そのプロパティで 利用可能な比較演算子 含む 等しい 小なりなどが リストに表示されています ここでは日付プロパティに 「小なり」「大なり」の比較演算子を タイトルプロパティに 「含む」「等しい」の比較演算子を リストに表示しています クエリプロパティは プロパティと 比較演算子の 各組み合わせを 比較演算子マッピング型という 任意の型にマッピングします CoreDataを使用しているため NSPredicateを使用します カスタムデータベースまたは REST APIを使用した場合 自身の比較演算子型を設計して それを代用することも 可能です こちらが本のクエリプロパティ を設定するコードです BooksQueryをEntityProperty Queryプロトコルに適合させます QueryPropertiesの Resultビルダーを使って staticなvarプロパティ を実装します 各エントリでは 問い合わせ可能な PropertyのkeyPathを特定し その中でそのプロパティに 適用可能な各比較演算子を 指定します 比較演算子のマッピング型として NSPredicateを選んだため それぞれの比較演算子に対し NSPredicateを提供します システムがAppにクエリの 結果を返すように要求すると ここで構築している NSPredicatesが 返されることになります ソートにも類似した 定義があります これは私のモデルが本を 並べ替えることができる すべてのプロパティの リストです この場合 タイトル 読書日 公開日でソート できるようにしています 最後にデータベースに クエリを実行して エンティティのマッチングを返す entity(matching:)を実装します このメソッドは先に定義した クエリパラメータで使った 比較演算子 マッピング型の配列 この場合NSPredicate を受け取ります これらのPredicateはエンティティの プロパティのどのような基準で クエリを実行したいか 示しています 「and」と「or」 のどちらで Predicateを組み合わせるかを示す モード ソートするキーパス オプションで結果数の 制限を示すことができます 実装することで これらのパラメータを使用して CoreDataデータベースに 対してクエリを実行します このプロパティクエリで 何をすることができるでしょうか ライブラリからランダムに 本を選んで読むことができます 20世紀初頭に出版された 彼らの本をすべて 見つけることができます ショートカット エコシステムを活用し 他のAppと連携させ Appをより便利なものにできます 例えば表計算Appを使用して 今年読んだ本をすべて CSVファイルに書き出すことができます グラフAppを使用して 過去10年間 毎年何冊の本を読んだかを グラフにすることもできます これはほんの始まりに 過ぎません このような Appインテントの 深い導入により 顧客はAppを使用して 必要なことを行うことができ ワークフローの 重要な一部となります 例えばグラフの作成など これらの統合はそれぞれ 自身で構築する 必要のない機能です インテントが 実行された際に Appで結果を表示したり 話したり Siriのリクエストやショートカットなど 曖昧さを解決するために ユーザーとのインタラクションが 必要になる場合があります Appインテントは次のような インタラクションに数多く対応しています インテント完了時に ユーザーにテキストや 音声でフィードバックを 取得するダイアログや ビジュアルフィードバックを 提供するスニペットなどです インテントのパラメータの値を 明確にするための ユーザーに求める要求値 および曖昧性解消 トランザクション的または 破壊的なインテントで パラメータ値の検証 またはユーザーとの 確認を行う 確認があります ダイアログはインテントを 実行する人に対して 音声またはテキストによる 応答を提供します 音声による体験でインテントを うまく機能させるには ダイアログを用意する ことがとても重要です 先ほどの AddBookインテントに 本のタイトルを 尋ねるときに発言する needsValueDialogと performメソッドから 戻されるResultダイアログを 追加します 複数のプラットフォーム を通して ショートカットやSiriによって 読み取られたり表示されたりします スニペットはダイアログの 視覚的な表現となり インテントの結果に 視覚的な表現を 追加することができます スニペットを使用するには インテントの結果にtrailing closureとして 選択したSwiftUIビュー を追加するだけです ウィジェットのように SwiftUIビューはアーカイブされ ショートカットやSiriに アーカイブされ送信されます Appインテントは requestValueを投げて ユーザーに値を要求する こともサポートします 例えば オプションになることもある パラメータに値が 必要な場合に便利です ここでrequestValueは 文字列検索で 複数の本が 返ってきたときに役立ちます この場合 本の検索を絞り込むために 著者で検索します requestValueでthrowが できるエラーがあるため それによりユーザーに プロンプトをし 著者名を更新して アクションを返します 曖昧性解消は 一方で パラメータの値のセットから ユーザーが選択する 必要がある際に機能します これによりAddBookアクションで 複数の可能性のある 結果を処理する方法が さらに改善されました ここでは生成された本から 著者名のリストを取得し それらの可能性のある値において 曖昧性解消をリクエストします ユーザーはこれらの中から 選択することを要求され 結果を返します そしてAppインテントは異なる 2つの確認をサポートします 1つ目は パラメータ値の確認です この値はその値であるべきと 推測された値ですが 念のため確認します 本を追加する際 タイトルで本を探すために 呼び出した Webサービスが2件ほど 適合するものを返しますが そのうちの1件が圧倒的に 人気がある場合があります このような場合 ユーザーはその人気のある 本を追加したはずですが 念のため間違っていないか 確認をします そのために タイトルパラメータに requestConfirmation を呼び出します 2つ目は インテントの結果の 確認です これは たとえば 注文するときなどに最適です 読書Appを マネタイズして ストアを介した注文を 追加したい場合 注文が間違っていないか 確認したいかと思います そのために インテントで requestConfirmationを呼び出し 注文を送信します こちらもスニペットを指定して 注文のプレビューを表示します requestConfirmationは ユーザーが確認せずに キャンセルした場合 エラーを投げるため 呼び出しに 「try」がついています このセッションを終える前に フレームワークを採用する際に 知っておくべきいくつかの Appインテントの アーキテクチャの側面について 説明したいと思います Appインテントを 構築する方法には App内と それとは別のExtensionの 2つがあります これらのうち Appに直接インテントを 実装することが 最もシンプルな方法です フレームワークや コードの複製が不要であり 処理過程においても調整等が 必要ないため 優れた方法です Appを使うことで より高いメモリ制限と オーディオの再生など Extensionでは難しい種類の 作業を行うことができます インテントにopenAppWhenRunを 実装してtrueを返す場合 フォアグラウンドで Appを実行することができます そうでない場合は バックグラウンドで実行されます バックグラウンドで実行する場合 性能を最大化するために シーンがない特別モードで Appが起動されます 実際にAppに バックグラウンド Appインテントを実装する場合 シーンのサポートも 実装することを強く推奨します または ExtentionでAppインテント を構築することも可能です これには2つほど 利点があります ExtensionはAppインテントを 処理するだけで Appをロードする 必要がないため 軽量です Focusインテントを 処理する場合 Extensionを使用すると 集中モードが変更された際に Appがフォアグラウンドで 実行することなしに Extensionで実行される インテントをすぐに取得できます Extensionは 新しいターゲットを追加し いくつかのコードを フレームワークに移動し AppとExtension間の 調整する 必要があるため 少し処理が必要となります App Intents Extension を作成するには Xcodeの File > New Targetから App Intents Extension を選択します Appインテントでは コードが 唯一のソースとなります Appインテントは構築時に インテント エンティティ クエリ パラメータに関する情報を 静的に抽出することで この素晴らしいデベロッパ 体験を実現します Xcodeは構築中に コード上で実行する際に Swiftコンパイラから 受け取った情報を含め AppまたはExtension バンドルの内部に メタデータファイル を生成します このすべてが機能させるには App Intents型を フレームワークではなくターゲットまたは Extensionに直接保持するようにします ローカライズされた文字列は App Intents型が 存在するのと同じ バンドル内の stringsファイルに入れます SiriKitインテントを搭載した 既存のAppをお持ちの方で バージョンアップしたい方 ウィジェットとの連携や メッセージングや メディアなどの領域に インテントを採用する場合は SiriKit Intentsフレームワーク を使い続ける必要があります Siriとショートカットにカスタム インテントを追加する場合は 先にAppインテントにアップグレード しておくと良いでしょう SiriKitの インテント定義ファイルにある Convert to App Intentボタン をクリックすると アップグレード処理を 開始することができます AppをAppインテントを使った ショートカットに対応させることは デベロッパとしてのレバレッジを 最大化するための素晴らしい方法です Appインテントを採用するための 少しの作業を行うだけで 大きな価値を生み出す ことができるからです ご視聴いただきありがとうございます 今日からAppインテントを お試しいただき 皆さまのご意見を いただければと思います この新しいフレームワークが Appを使う人たちに 驚きや喜び 力を与えるのに 役立つことを願います WWDCで素晴らしい時間と 読書を楽しんでください! ♪
-
-
5:33 - Open Currently Reading
struct OpenCurrentlyReading: AppIntent { static var title: LocalizedStringResource = "Open Currently Reading" @MainActor func perform() async throws -> some IntentResult { Navigator.shared.openShelf(.currentlyReading) return .result() } static var openAppWhenRun: Bool = true }
-
6:42 - App Shortcuts
struct LibraryAppShortcuts: AppShortcutsProvider { static var appShortcuts: [AppShortcut] { AppShortcut( intent: OpenCurrentlyReading(), phrases: ["Open Currently Reading in \(.applicationName)"], systemImageName: "books.vertical.fill" ) } }
-
7:11 - Shelf Enum
enum Shelf: String { case currentlyReading case wantToRead case read } extension Shelf: AppEnum { static var typeDisplayRepresentation: TypeDisplayRepresentation = "Shelf" static var caseDisplayRepresentations: [Shelf: DisplayRepresentation] = [ .currentlyReading: "Currently Reading", .wantToRead: "Want to Read", .read: "Read", ] }
-
7:49 - Open Shelf
struct OpenShelf: AppIntent { static var title: LocalizedStringResource = "Open Shelf" @Parameter(title: "Shelf") var shelf: Shelf @MainActor func perform() async throws -> some IntentResult { Navigator.shared.openShelf(shelf) return .result() } static var parameterSummary: some ParameterSummary { Summary("Open \(\.$shelf)") } static var openAppWhenRun: Bool = true }
-
10:56 - Book Entity
struct BookEntity: AppEntity, Identifiable { var id: UUID var displayRepresentation: DisplayRepresentation { "\(title)" } static var typeDisplayRepresentation: TypeDisplayRepresentation = "Book" static var defaultQuery = BookQuery() }
-
12:29 - Book Query
struct BookQuery: EntityQuery { func entities(for identifiers: [UUID]) async throws -> [BookEntity] { identifiers.compactMap { identifier in Database.shared.book(for: identifier) } } }
-
13:16 - Open Book
struct OpenBook: AppIntent { @Parameter(title: "Book") var book: BookEntity static var title: LocalizedStringResource = "Open Book" static var openAppWhenRun = true @MainActor func perform() async throws -> some IntentResult { guard try await $book.requestConfirmation(for: book, dialog: "Are you sure you want to clear read state for \(book)?") else { return .result() } Navigator.shared.openBook(book) return .result() } static var parameterSummary: some ParameterSummary { Summary("Open \(\.$book)") } init() {} init(book: BookEntity) { self.book = book } }
-
13:40 - Suggested Entities and String Book Query
struct BookQuery: EntityStringQuery { func entities(for identifiers: [UUID]) async throws -> [BookEntity] { identifiers.compactMap { identifier in Database.shared.book(for: identifier) } } func suggestedEntities() async throws -> [BookEntity] { Database.shared.books } func entities(matching string: String) async throws -> [BookEntity] { Database.shared.books.filter { book in book.title.lowercased().contains(string.lowercased()) } } }
-
15:11 - Add Book Intent
struct AddBook: AppIntent { static var title: LocalizedStringResource = "Add Book" @Parameter(title: "Title") var title: String @Parameter(title: "Author Name") var authorName: String? @Parameter(title: "Recommended By") var recommendedBy: String? func perform() async throws -> some IntentResult & ReturnsValue<BookEntity> & OpensIntent { guard var book = await BooksAPI.shared.findBooks(named: title, author: authorName).first else { throw Error.notFound } book.recommendedBy = recommendedBy Database.shared.add(book: book) return .result( value: book, openIntent: OpenBook(book: book) ) } enum Error: Swift.Error, CustomLocalizedStringResourceConvertible { case notFound var localizedStringResource: LocalizedStringResource { switch self { case .notFound: return "Book Not Found" } } } }
-
18:21 - Book Entity with Properties
struct BookEntity: AppEntity, Identifiable { var id: UUID @Property(title: "Title") var title: String @Property(title: "Publishing Date") var datePublished: Date @Property(title: "Read Date") var dateRead: Date? var recommendedBy: String? var displayRepresentation: DisplayRepresentation { "\(title)" } static var typeDisplayRepresentation: TypeDisplayRepresentation = "Book" static var defaultQuery = BookQuery() init(id: UUID) { self.id = id } init(id: UUID, title: String) { self.id = id self.title = title } }
-
20:59 - Books Property Query
struct BookQuery: EntityPropertyQuery { static var sortingOptions = SortingOptions { SortableBy(\BookEntity.$title) SortableBy(\BookEntity.$dateRead) SortableBy(\BookEntity.$datePublished) } static var properties = QueryProperties { Property(\BookEntity.$title) { EqualToComparator { NSPredicate(format: "title = %@", $0) } ContainsComparator { NSPredicate(format: "title CONTAINS %@", $0) } } Property(\BookEntity.$datePublished) { LessThanComparator { NSPredicate(format: "datePublished < %@", $0 as NSDate) } GreaterThanComparator { NSPredicate(format: "datePublished > %@", $0 as NSDate) } } Property(\BookEntity.$dateRead) { LessThanComparator { NSPredicate(format: "dateRead < %@", $0 as NSDate) } GreaterThanComparator { NSPredicate(format: "dateRead > %@", $0 as NSDate) } } } func entities(for identifiers: [UUID]) async throws -> [BookEntity] { identifiers.compactMap { identifier in Database.shared.book(for: identifier) } } func suggestedEntities() async throws -> [BookEntity] { Model.shared.library.books.map { BookEntity(id: $0.id, title: $0.title) } } func entities(matching string: String) async throws -> [BookEntity] { Database.shared.books.filter { book in book.title.lowercased().contains(string.lowercased()) } } func entities( matching comparators: [NSPredicate], mode: ComparatorMode, sortedBy: [Sort<BookEntity>], limit: Int? ) async throws -> [BookEntity] { Database.shared.findBooks(matching: comparators, matchAll: mode == .and, sorts: sortedBy.map { (keyPath: $0.by, ascending: $0.order == .ascending) }) } }
-
24:10 - Dialog
struct AddBook: AppIntent { static var title: LocalizedStringResource = "Add Book" @Parameter(title: "Title") var title: String @Parameter(title: "Author Name") var authorName: String? @Parameter(title: "Recommended By") var recommendedBy: String? func perform() async throws -> some IntentResult & ReturnsValue<BookEntity> & ProvidesDialog { guard var book = await BooksAPI.shared.findBooks(named: title, author: authorName).first else { throw Error.notFound } book.recommendedBy = recommendedBy Database.shared.add(book: book) return .result( value: book, dialog:"Added \(book) to Library!" ) } enum Error: Swift.Error, CustomLocalizedStringResourceConvertible { case notFound var localizedStringResource: LocalizedStringResource { switch self { case .notFound: return "Book Not Found" } } } }
-
24:25 - Snippet
struct AddBook: AppIntent { static var title: LocalizedStringResource = "Add Book" @Parameter(title: "Title") var title: String @Parameter(title: "Author Name") var authorName: String? @Parameter(title: "Recommended By") var recommendedBy: String? func perform() async throws -> some IntentResult & ShowsSnippetView { guard var book = await BooksAPI.shared.findBooks(named: title, author: authorName).first else { throw Error.notFound } book.recommendedBy = recommendedBy Database.shared.add(book: book) return .result(value: book) { CoverView(book: book) } } enum Error: Swift.Error, CustomLocalizedStringResourceConvertible { case notFound var localizedStringResource: LocalizedStringResource { switch self { case .notFound: return "Book Not Found" } } } }
-
24:50 - Request Value
struct AddBook: AppIntent { static var title: LocalizedStringResource = "Add Book" @Parameter(title: "Title") var title: String @Parameter(title: "Author Name") var authorName: String? @Parameter(title: "Recommended By") var recommendedBy: String? func perform() async throws -> some IntentResult { let books = await BooksAPI.shared.findBooks(named: title, author: authorName) guard !books.isEmpty else { throw Error.notFound } if books.count > 1 && authorName == nil { throw $authorName.requestValue("Who wrote the book?") } return .result() } enum Error: Swift.Error, CustomLocalizedStringResourceConvertible { case notFound var localizedStringResource: LocalizedStringResource { switch self { case .notFound: return "Book Not Found" } } } }
-
25:22 - Request Disambiguation
struct AddBook: AppIntent { static var title: LocalizedStringResource = "Add Book" @Parameter(title: "Title") var title: String @Parameter(title: "Author Name") var authorName: String? @Parameter(title: "Recommended By") var recommendedBy: String? func perform() async throws -> some IntentResult { let books = await BooksAPI.shared.findBooks(named: title, author: authorName) guard !books.isEmpty else { throw Error.notFound } if books.count > 1 { let chosenAuthor = try await $authorName.requestDisambiguation(among: books.map { $0.authorName }, dialog: "Which author?") } return .result() } enum Error: Swift.Error, CustomLocalizedStringResourceConvertible { case notFound var localizedStringResource: LocalizedStringResource { switch self { case .notFound: return "Book Not Found" } } } }
-
25:48 - Request Parameter Confirmation
struct AddBook: AppIntent { static var title: LocalizedStringResource = "Add Book" @Parameter(title: "Title") var title: String @Parameter(title: "Author Name") var authorName: String? @Parameter(title: "Recommended By") var recommendedBy: String? func perform() async throws -> some IntentResult & ReturnsValue<BookEntity> { guard var book = await BooksAPI.shared.findBooks(named: title, author: authorName).first else { throw Error.notFound } let confirmed = try await $title.requestConfirmation(for: book.title, dialog: "Did you mean \(book)?") book.recommendedBy = recommendedBy Database.shared.add(book: book) return .result(value: book) } enum Error: Swift.Error, CustomLocalizedStringResourceConvertible { case notFound var localizedStringResource: LocalizedStringResource { switch self { case .notFound: return "Book Not Found" } } } }
-
26:26 - Request Result Confirmation
struct BuyBook: AppIntent { @Parameter(title: "Book") var book: BookEntity @Parameter(title: "Count") var count: Int static var title: LocalizedStringResource = "Buy Book" func perform() async throws -> some IntentResult & ShowsSnippetView & ProvidesDialog { let order = OrderEntity(book: book, count: count) try await requestConfirmation(output: .result(value: order, dialog: "Are you ready to order?") { OrderPreview(order: order) }) return .result(value: order, dialog: "Thank you for your order!") { OrderConfirmation(order: order) } } }
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。