Avance.Lab

技術紹介

iOS 27のSiri AIに備えて ― はじめてのApp Intents

公開日:2026.08.21 更新日:2026.08.21

tag: スマートデバイス

こんにちは、n/aです。

今秋リリース予定のiOS 27では、Apple Intelligenceを活用した新しい「Siri AI」が登場します。

Siri AIでは、Siriとの会話を通して

  • 友人がメッセージで教えてくれたおすすめのカフェを探す
  • 過去のEメールから次の打ち合わせ予定を見つける
  • 家族との夏休み旅行の写真を表示する

といったように、他のアプリの機能を自然に利用できるようになります。
自分のアプリがSiri AIとスムーズに連携できるかが、今後のユーザー体験に直結してきそうです。

そこで今回は、Siri AIに備える第一歩として、アプリに App Intents を追加していく流れを紹介します。

App Intentsとは

App Intentsは、アプリ外部(Siri、Spotlight、Shortcutsアプリ、ウィジェットなど)から、
アプリのアクションやコンテンツを使えるようにするためのフレームワークです。

基本となる要素は3つです。

  • Intent: アプリが提供するアクションを定義
  • Entity: アプリのデータオブジェクトをIntentから扱えるようにする専用のモデルクラス
  • Shortcut: IntentをSiriやShortcutsアプリから呼び出せるようにする橋渡しの仕組み

この他に、AppSchemaやiOS 27から利用可能なAPIを使うことでよりSiri AIに最適化できますが、
まずはApp IntentsだけでもSiri AIとの基本的な連携の準備が整います。

題材となるアプリ

今回の題材は、よくあるシンプルなTodoアプリです。

  • SwiftUI + SwiftData
  • Todoの追加・一覧表示・完了切り替えができる
  • 各Todoはタイトル・対応期限・完了状態を持つ

このアプリに、Siriやショートカットアプリから呼び出せるようにApp Intentsを追加していきます。
(Todoアプリそのものについては本記事では取り扱いません。)

作成するIntent

チュートリアルとして、Todoアプリの中でも特によく使う2つのアクションをIntent化します。

  • Todoを追加する
  • Todoの状態を完了にする

どちらも、アプリを開かなくてもSiriやショートカットから直接実行できるようにします。

Todoを追加するIntent

まずは「Todoを追加する」というアクションから、App Intentsの基本とショートカットへの登録までを実装していきます。

1. AddTodoIntent ― Todo追加のアクションを定義する

アプリ外部からユーザーが実行したいアクション1つに対して1つのIntentを定義します。
AppIntent に準拠した構造体が、Siriやショートカットから呼び出せる「アクション」の実体です。
最小構成は titleperform() です。
今回はアプリ画面を開かずに実行するため、 openAppWhenRunfalse にします。

import AppIntents
import SwiftData

struct AddTodoIntent: AppIntent {
    static var title: LocalizedStringResource = "Todoを追加"
    static var description = IntentDescription("新しいTodoを1件追加します。")
    static var openAppWhenRun: Bool = false

    @Parameter(title: "タイトル")
    var titleText: String

    @Parameter(title: "期限", default: Date.now.addingTimeInterval(3600*24))
    var dueDate: Date
    
    static var parameterSummary: some ParameterSummary {
        Summary("\(\.$dueDate)までに「\(\.$titleText)」をやる")
    }

    @MainActor
    func perform() async throws -> some IntentResult & ProvidesDialog {
        let context = ModelContext(TodoModelContainer.shared)
        let item = TodoItem(
            title: titleText,
            dueDate: dueDate
        )
        context.insert(item)
        try context.save()
        
        return .result(
            dialog: "「\(titleText)」(期限:\(dueDate, format: .dateTime.year().month().day().hour().minute()))を追加しました。"
        )
    }
}

💡 実装のポイント

実行時にユーザーに「タイトル」「期限」を入力してもらうため、入力項目と対応するプロパティに @Parameter を付与します。
期限(dueDate)にはデフォルト値(Intent実行時刻から24時間後)を用意しました。
これにより、実行時に期限の入力が省略されます。
Intentを実行するとタイトルの入力UIが表示されます。

parameterSummary では、 \.$dueDate\.$titleText のように、パラメータのキーパスを文章の中に埋め込んでいます。
こうしておくと、Shortcutsアプリの編集画面でパラメータが以下のように文章として表示され、わかりやすくなります。

perform() の中では、受け取ったパラメータを使って TodoItem (Todo1件を表すアプリのモデルクラス)を作成し、SwiftDataに保存しています。
これはアプリを開いてTodoを追加するときと同じ処理です。
returnで返した文字列はIntentの実行結果として表示されます。

2. TodoShortcutsProvider ― Siriやショートカットへの橋渡し

作成したIntentをSiriやShortcutsアプリから呼び出しやすくするため、 AppShortcutsProvider を作成し、Intent毎に AppShortcut を登録します。

import AppIntents

struct TodoShortcutsProvider: AppShortcutsProvider {
    static var appShortcuts: [AppShortcut] {
        return [
            AppShortcut(
                intent: AddTodoIntent(),
                phrases: [
                    "\(.applicationName)でTodoを追加",
                    "新しいTodoを\(.applicationName)で作成"
                ],
                shortTitle: "Todo追加",
                systemImageName: "plus"
            )
        ]
    }

    static var shortcutTileColor: ShortcutTileColor { .blue }
}

💡 実装のポイント

AppShortcutphrases に音声フレーズを登録しておくと、「(アプリ名)でTodoを追加」のような発話でSiriから直接実行できるようになります。
\(.applicationName) の部分はアプリの実際の名前に置き換わります。

shortTitlesystemImageName でShortcutsアプリでの見た目をカスタマイズします。

ここまでで、App Intentsの一番基本的な形である「Intentを作り、パラメータを定義し、ショートカットに登録する」という流れを一通り実装できました。

Todoの状態を完了にするIntent

次に「指定したTodoの状態を完了にする」というアクションを作ります。
こちらは「アプリの中のどのデータを指しているか」をSiriに伝える必要があるので、先程より一段階複雑になります。

3. TodoEntity ― アプリデータをSiriやShortcutアプリで扱える形式にする

このアプリではTodoを TodoItem というモデルクラスで表現しますが、そのままではSiriやShortcutアプリに渡せません。
SiriやShortcutアプリが扱えるカスタムデータは AppEntity に準拠した型だけなので、 TodoItem に対応する TodoEntity を用意します。

import AppIntents
import SwiftData

struct TodoEntity: AppEntity {
    static var typeDisplayRepresentation: TypeDisplayRepresentation {
        TypeDisplayRepresentation(name: "Todo")
    }
    static var defaultQuery = TodoEntityQuery()

    let id: UUID
    let title: String
    var dueDate: Date
    let isDone: Bool

    init(_ item: TodoItem) {
        self.id = item.id
        self.title = item.title
        self.dueDate = item.dueDate
        self.isDone = item.isDone
    }

    var displayRepresentation: DisplayRepresentation {
        return DisplayRepresentation(
            title: "\(title)",
            subtitle: "期限: \(dueDate, format: .dateTime.month().day().hour().minute())"
        )
    }
}

💡 実装のポイント

displayRepresentation はSiriやShortcutsアプリ上で TodoEntity をどう表示するかを指定します。
実際に表示すると以下のようになります。

defaultQueryTodoEntityQuery() を指定していますが、これについては後ほど説明します。

4. MarkTodoDoneIntent ― Todo完了のアクションを定義する

完了したいTodo ( TodoEntity ) をパラメータとして受け取り、状態を変更するアクションを定義します。

import AppIntents
import SwiftData

struct MarkTodoDoneIntent: AppIntent {
    static var title: LocalizedStringResource = "Todoを完了する"
    static var description = IntentDescription("指定したTodoを完了にします。")
    static var openAppWhenRun: Bool = false

    @Parameter(title: "Todo")
    var todo: TodoEntity

    static var parameterSummary: some ParameterSummary {
        Summary("\(\.$todo)を完了する")
    }

    @MainActor
    func perform() async throws -> some IntentResult & ProvidesDialog {
        let context = ModelContext(TodoModelContainer.shared)
        let targetID = todo.id
        let descriptor = FetchDescriptor<TodoItem>(
            predicate: #Predicate { $0.id == targetID }
        )
        guard let item = try context.fetch(descriptor).first else {
            throw TodoIntentError.notFound
        }
        item.isDone = true
        try context.save()

        return .result(dialog: "「\(item.title)」を完了しました。")
    }
}

enum TodoIntentError: Swift.Error, CustomLocalizedStringResourceConvertible {
    case notFound

    var localizedStringResource: LocalizedStringResource {
        switch self {
        case .notFound:
            return "指定されたTodoが見つかりませんでした。"
        }
    }
}

💡 実装のポイント

ユーザーから受け取るパラメータの型を先程の TodoEntity にしています。
perform() では TodoEntityid を使って一致する TodoItem を検索し、状態を完了に変更します。

このIntentを実行すると、候補となるTodoが提示され、ユーザーはその中から1件を選択できます。

この時に候補となるTodoを決定するのが、先程 TodoEntitydefaultQuery に指定した TodoEntityQuery です。

5. TodoEntityQuery ― Entityを検索する

EntityQuery は、Entityを選択するパラメータに対して「候補として何を出すか」「IDから実データをどう引くか」を決める仕組みです。

import AppIntents
import SwiftData

struct TodoEntityQuery: EntityQuery {
    @MainActor
    func entities(for identifiers: [TodoEntity.ID]) async throws -> [TodoEntity] {
        let context = ModelContext(TodoModelContainer.shared)
        let descriptor = FetchDescriptor<TodoItem>(
            predicate: #Predicate { identifiers.contains($0.id) }
        )
        return try context.fetch(descriptor).map(TodoEntity.init)
    }

    @MainActor
    func suggestedEntities() async throws -> [TodoEntity] {
        let context = ModelContext(TodoModelContainer.shared)
        let descriptor = FetchDescriptor<TodoItem>(
            predicate: #Predicate { $0.isDone == false },
            sortBy: [SortDescriptor(\.dueDate, order: .forward)]
        )
        return try context.fetch(descriptor).map(TodoEntity.init)
    }
}

💡 実装のポイント

suggestedEntities() は、SiriやShortcutアプリの編集画面でユーザーに提示する候補一覧を返します。
ここでは未完了のTodoを期限が近い順に返しています。

TodoShortcutsProviderへの登録はAddTodoIntentと同様です。

ここまでが、アプリ内のデータを取得・操作する基本的なIntentの実装になります。

発展編 ― 結果表示をカスタマイズする

上記の AddTodoIntentMarkTodoDoneIntent は、メッセージだけを結果として返していますが、
App Intentsでは実行結果としてSwiftUIのカスタムビューを返すこともできます。

例えば「未完了のTodo一覧を取得する」Intentであれば、List形式のカスタムビューを結果として表示させるとわかりやすくなります。

@MainActor
func perform() async throws -> some IntentResult & ReturnsValue<[TodoEntity]> & ProvidesDialog & ShowsSnippetView {
    let context = ModelContext(TodoModelContainer.shared)
    let descriptor = FetchDescriptor<TodoItem>(
        sortBy: [SortDescriptor(\.dueDate, order: .forward)]
    )
    var items = try context.fetch(descriptor)
    items = items.filter { $0.isDone == false }

    let entities = items.map(TodoEntity.init)
    return .result(
        value: entities,
        dialog: "未完了のTodoは\(entities.count)件です。",
        view: ResultView(items: items)
    )
}

perform() の戻り値に ShowsSnippetView を追加し、任意のカスタムビュークラス(ここでは ResultView )を渡します。
ResultView は例えば以下のように実装します。期限が過ぎているものは赤字で表示します。

struct ResultView: View {
    var items: [TodoItem]
    
    var body: some View {
        ForEach(items) { item in
            VStack(alignment: .leading, spacing: 4) {
                Text(item.title)
                    .foregroundStyle(.primary)
                
                HStack(spacing: 0) {
                    Text(item.dueDate, format: .dateTime.month().day().hour().minute())
                        .foregroundStyle(isOverdue(item) ? .red : .secondary)
                    Spacer()
                }
                .font(.caption2)
                
                Divider()
                    .foregroundStyle(.separator)
                    .padding(.top, 4)
            }
            .padding(.top, items.first == item ? 8 : 0)
            .padding(.leading, 20)
        }
    }
    
    private func isOverdue(_ item: TodoItem) -> Bool {
        !item.isDone && item.dueDate < .now
    }
}

実際の表示は以下のようになります。

まとめ

App Intentsの導入は、iOS 26の今からできるSiri AIへの準備であるとともに、
多様な経路からアプリを利用しやすくし、UXを向上させる取り組みでもあります。

まずはアプリの主要な機能からApp Intentsを導入してみるのがおすすめです。

n/a

主にモバイルアプリを開発しています。
最近はカメラに興味があります。

関連記事