🧩

App Intentsチュートリアル - Part 3 プロパティと検索、フィルタリング

2024/12/04に公開

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

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

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

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

この機能を使えば、ユーザーはアプリを開かずに欲しい情報を簡単に取り出したり、特定条件に応じたデータ操作をSiri/Apple Intelligenceを通じて直接行えるようになります。

読書アプリの説明

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

48

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

プロパティでエンティティ情報を公開する

エンティティの情報をより多く公開し、ユーザーがその情報を基に検索やフィルタリングできるようにする方法について説明します。

プロパティ

エンティティはプロパティをサポートしており、ユーザーに公開したい エンティティの追加情報 を保持します。

163

今回の場合は 本の著者・出版日・読書日・推薦者を追加し ショートカットでそれらのプロパティ を使うことができます。

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

BookEntity に プロパティを追加するには @Property というプロパティラッパーを使用します。

プロパティはパラメータと同じ型に対応しており、それぞれローカライズされたタイトルを取ります。

これらの新しいプロパティにより、ユーザーは本のエンティティを処理する際に、ショートカットのマジック変数を使用してそれぞれ新しい情報を引き出すことができるようになりました。

169

先ほどの AddBook インテントを使用する際

170

新たに追加された 本の著者や出版日をショートカットで使用することができます。

172

プロパティクエリで高度な検索を可能にする

プロパティとクエリを組み合わせる際に、Appは自動的にこの柔軟な述語エディタUIを備えた 非常に強力な検索とフィルタアクションをショートカットに対し得ることができます。

177

これでユーザーは読書日・タイトル・著者名などに基づいて本を検索し、フィルタリングできるようになりました。

例えば Delia Owensの本をすべて探す、といったことが可能になります。

180

Sort byやLimit オプションを使えば、「Delia Owensの最近出版された3冊の本を探す」など、さらに高度なクエリに対応することができます。

ユーザーはこの構成要素を使って、「コレクションの中で最も著名な3名の作家を見つける」など、非常に優れた機能が利用できます。

プロパティクエリを実装する3ステップ

これらすべてを可能にするために、プロパティクエリという別の種類のクエリを採用する必要があります。

プロパティクエリは文字列や識別子に基づくのではなく、エンティティ内のプロパティに 基づいてエンティティを検索します。

プロパティクエリの実装には 3つのステップがあります。

  1. まず最初に QueryProperties を宣言して、そのプロパティを使用してエンティティを検索する方法を指定します。

  2. 次にソートオプション を追加し、クエリ結果のソート方法 を定義します。

  3. 最後に entities(matching:) を実装して 検索を実行します。

1. QueryProperties の設定

QueryProperties は Appインテントが このクエリに関連する エンティティで検索できる あらゆる方法を表示します。

185

エンティティのプロパティと そのプロパティで 利用可能な比較演算子 含む 等しい 小なりなどが リストに表示されています。 ここでは日付プロパティに 「小なり」「大なり」の比較演算子を、タイトルプロパティに 「含む」「等しい」の比較演算子を リストに表示しています。

186

QueryProperties は プロパティと 比較演算子の 各組み合わせを 比較演算子マッピング型という 任意の型にマッピングします。

こちらが本の QueryProperties を設定するコードです。

struct BookQuery: EntityPropertyQuery {
    ...

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

    ...
}

BooksQueryEntityPropertyQuery プロトコルに適合させ、QueryProperties のResultビルダーを使って staticvarプロパティ を実装します。

各エントリでは 問い合わせ可能な Propertyの keyPath を特定し、

190

その中でそのプロパティに 適用可能な各比較演算子を指定します。

191

比較演算子のマッピング型として NSPredicate を選んだため、それぞれの比較演算子に対し NSPredicate を提供します。

192

システムがAppにクエリの結果を返すように要求すると、ここで構築している NSPredicates が 返されることになります。

本アプリではCoreDataを使用しているため NSPredicate を使用しましたが、カスタムデータベースまたは REST APIを使用した場合、自身の比較演算子型を設計してそれを代用することも可能です。

2. クエリ結果のソート方法を定義

ソートにも類似した定義があります。

struct BookQuery: EntityPropertyQuery {
    static var sortingOptions = SortingOptions {
        SortableBy(\BookEntity.$title)
        SortableBy(\BookEntity.$dateRead)
        SortableBy(\BookEntity.$datePublished)
    }

    ...
}

これは私のモデルが本を 並べ替えることができる すべてのプロパティのリストです。この場合、タイトル・読書日・公開日でソートできるようにしています。

3. クエリの実行

最後にデータベースに クエリを実行して エンティティのマッチングを返す entity(matching:) を実装します。

struct BookQuery: EntityPropertyQuery {
    ...

    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) })
    }
}
  • このメソッドは先に定義した クエリパラメータで使った 比較演算子 マッピング型(この場合 NSPredicate )の配列を受け取ります

    • これらのPredicateはエンティティの プロパティのどのような基準で クエリを実行したいか 示しています。
  • 「and」と「or」 のどちらで Predicateを組み合わせるかを示す モード、ソートするキーパス オプションで結果数の 制限を示すことができます 。

これらのパラメータを使用して CoreDataデータベースに 対してクエリを実行します。

`BookQuery` のコード全体
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) })
    }
}

プロパティクエリでできること

このプロパティクエリで 何をすることができるでしょうか?

ライブラリからランダムに本を選んで読むことができます。

20世紀初頭に出版された彼らの本をすべて 見つけることができます。

205

ショートカットのエコシステムを活用し、他のAppと連携させ、Appをより便利なものにできます。

例えば表計算Appを使用して今年読んだ本をすべて CSVファイルに書き出すことや、
グラフAppを使用して「過去10年間、毎年何冊の本を読んだか」をグラフにすることもできます

210

このようにインテントを実装することで、ワークフローの一部として機能し、ユーザーはアプリを横断的に活用して必要なことを柔軟に実現できるようになります。

次のステップ

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

https://zenn.dev/shu223/articles/appintents_book4

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

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

Discussion