🧩

App Intentsチュートリアル - Part 4 ユーザーインタラクション

2024/12/05に公開

WWDC22の "Dive into App Intents" (Appインテントの詳細) というセッションで、読書アプリにApp Intentsを実装していく事例があり、具体的で非常にわかりやすかったので、記事としてまとめてみました。

Part 1」では、読書アプリの特定のタブを素早く開くインテントの実装を通して、App Intentsの基礎について解説しました。

Part 2」では、エンティティとクエリを利用し、「ユーザーが選択した本を開くインテント」「ライブラリに本を追加するインテント」の実装方法を解説しました。また「ライブラリに本を追加し、その本を開く」インテント連携についても解説しました。

Part 3」では、エンティティのプロパティを用いて、例えば読書日・タイトル・著者名などに基づいて本を検索・フィルタリングして表示したりできるようにする方法について解説しました。これにより、ユーザーはなど、アプリの外でもインテントを通じて柔軟な条件でデータを活用できるようになります。

本記事「Part 4」がいよいよシリーズ最終回となります。インテントはアプリの外でアプリの機能を利用できるようにするものですが、インテントの実行結果を表示したり、ユーザー判断を要求したりといったユーザーインタラクションの方法について解説します。

読書アプリの説明

「読みたい本(Want to Read)」「読んだ本(Read)」「現在読んでいる本(Currently Reading)」を記録するアプリ。

48

アプリには3つのタブがあり、それぞれのリストを "Shelf" とセッション内では呼んでいる。

インテントとユーザーインタラクション

インテントが 実行された際にAppで結果を表示したり、話したり、Siriのリクエストやショートカットなど曖昧さを解決するために、ユーザーとのインタラクションが必要になる場合があります。

Appインテントは次のような数多くのインタラクションに対応しています:

  • Dialog: インテント完了時にユーザーにテキストや 音声でフィードバックを取得するダイアログ
  • Snippets: ビジュアルフィードバックを提供するスニペット
  • Request Value: インテントのパラメータの値を 明確にするための ユーザーに求める要求値
  • Disambiguation: 曖昧性解消/トランザクション的または破壊的なインテントで パラメータ値の検証
  • Confiermation: ユーザーとの確認を行う

ダイアログ

ダイアログはインテントを 実行する人に対して 音声またはテキストによる 応答を提供します。

音声による体験でインテントを うまく機能させるには ダイアログを用意する ことがとても重要です。

先ほどの AddBookインテントに、本のタイトルを 尋ねるときに発言する needsValueDialogと、 performメソッドから 戻されるResultダイアログを 追加します。

221

複数のプラットフォーム を通して ショートカットやSiriによって 読み取られたり表示されたりします。

(コード全体)

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"
            }
        }
    }
}

スニペット

スニペットはダイアログの 視覚的な表現となり インテントの結果に 視覚的な表現を 追加することができます。

224

スニペットを使用するには インテントの結果にtrailing closureとして選択したSwiftUIビュー を追加するだけです。

ウィジェットのように、SwiftUIビューはアーカイブされショートカットやSiriに送信されます。

(コード全体)

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"
            }
        }
    }
}

Request Value

Appインテントは requestValuethrow し、ユーザーに値を要求することもサポートします。例えば、パラメータの値が時折オプションとなる場合に便利です。

232

ここでは、文字列検索で複数の書籍が返された場合にrequestValueが役立ちます。

この場合、書籍検索を絞り込むために著者を尋ねます。requestValueはエラーを返すので、ユーザーに尋ね、著者の名前を更新してアクションを再実行します。

(コード全体)

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"
            }
        }
    }
}

曖昧性解消(Disambiguation)

曖昧性解消はパラメータの値のセットからユーザーが選択する必要がある際に機能します。

245

これによりAddBookアクションで 複数の可能性のある結果を処理する方法がさらに改善されます。

ここでは生成された本から 著者名のリストを取得し、それらの可能性のある値において曖昧性解消をリクエストします。

ユーザーはこれらの中から選択することを要求され、結果を返します。

(コード全体)

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"
            }
        }
    }
}

確認

そしてAppインテントは異なる 2つの確認をサポートします。

1つ目は パラメータ値の確認です。

247

この値はその値であるべきと推測された値ですが、念のため確認します。

本を追加する際、タイトルで本を探すために呼び出したWebサービスが2件ほど適合するものを返しますが、そのうちの1件が圧倒的に人気がある場合があります。このような場合 ユーザーはその人気のある本を追加したはずですが、念のため間違っていないか 確認をします。

そのためにタイトルパラメータに requestConfirmation を呼び出します。

248

(コード全体)

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"
            }
        }
    }
}

2つ目は インテントの結果の 確認です。

249

これは たとえば 注文するときなどに最適です。

読書Appを マネタイズして ストアを介した注文を追加したい場合、注文が間違っていないか確認したいかと思います。そのためにインテントで requestConfirmationを呼び出し注文を送信します。

こちらもスニペットを指定して 注文のプレビューを表示します。

requestConfirmationは ユーザーが確認せずに キャンセルした場合 エラーを投げるため 呼び出しに 「try」がついています。

(コード全体)

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

ひとりアドベントカレンダーやってます!

こちらはひとりで全日埋めるアドベントカレンダー iOS by shu223 - Qiita Advent Calendar 2024 - Qiita の5日目の記事です。がんばって完走目指すので、カレンダーの購読や記事のLikeをポチッとしていただけると嬉しいです。

Discussion