🐾

App Intents はじめの三歩

2024/11/14に公開

App Intents とは

  • アプリの「意図(インテント)」をシステムに伝えるもの
  • アプリの機能をアプリ外の様々な場所から使えるようになる


WWDC24 "Bring your app’s core features to users with App Intents" より

App Intentsに「今」取り組むモチベーション

App Intents対応 ≒ Apple Intelligence対応

Siriがアプリの機能を使ってくれるようになる(iOS 18.?

inline
WWDC24 "Bring your app’s core features to users with App Intents" より

どの機能をインテントとして切り出せばいいの?

全部

inline
WWDC24 "Design App Intents for system experiences" より

アシスタントスキーマ(Assistant Schemes)の話

inline
WWDC24 "Bring your app’s core features to users with App Intents" より

本発表の位置付け

  • App Intentsの概念とApple Intelligenceの関係はわかった
  • 「全機能切り出そう」というAppleの言い分もわかった
  • でもどこから始めたらいいの?

→ はじめの三歩目 [1] ぐらいまでをナビゲーションする


一歩目: アプリを開くだけのインテント

実装

// AppIntentに準拠
struct OpenAppIntent: AppIntent {
    // タイトルは必須
    static var title: LocalizedStringResource = "Open Hoge"
    
    // インテント実行時にアプリを起動する
    static var openAppWhenRun: Bool = true
    
    // インテント実行時の処理
    func perform() async throws -> some IntentResult {
        // 何もしない
        return .result()
    }
}
  • 既存実装に手をいれる必要がない
  • 実質5行程度

動作確認

  • インテント ≒ (ショートカットアプリにおける)アクション
  • アプリをインストールするだけでリストに出てくるようになる


二歩目: 入力を持つインテント

動画ファイルを入力に受け取ってアプリを開くインテント

インテント(アクション)を実行 → 動画選択 → 選択した動画ファイルを入力としてアプリ起動

inline

実装

struct TrimSilenceIntent: AppIntent {
    static var title: LocalizedStringResource = ...
    static var openAppWhenRun: Bool = true

    // 入力パラメータとして動画ファイルを受け取る
    @Parameter(title: "Video File", supportedTypeIdentifiers: ["public.movie"])
    var video: IntentFile

    @MainActor
    func perform() async throws -> some IntentResult {
        // 動画ファイルのURLにアクセス
        guard let videoURL = video.fileURL else { ... }                
        // 実際の処理
        ...
        return .result()
    }
}

@Parameter プロパティラッパー

@Parameter(title: "Video File", supportedTypeIdentifiers: ["public.movie"])
var video: IntentFile

これを指定したプロパティは、インテントのパラメータ(= アクションのパラメータ)としてショートカットアプリに表示されるようになる

`@Parameter` プロパティラッパーの定義
  • IntentParametertypealias
public typealias Parameter = IntentParameter
  • IntentParameter の定義:
@propertyWrapper final public class IntentParameter<Value> : @unchecked Sendable where Value : _IntentValue, Value : Sendable {
    final public let defaultValue: Value.UnwrappedType?
    final public let title: LocalizedStringResource
    final public var isOptional: Bool { get }
    final public var projectedValue: IntentParameter<Value> { get }
    final public var wrappedValue: Value
}
  • @Parameter(title:supportedTypeIdentifiers:) に相当するイニシャライザの定義:
public convenience init(title: LocalizedStringResource, description: LocalizedStringResource? = nil, default defaultValue: Value.UnwrappedType? = nil, supportedTypeIdentifiers: [String] = ["public.item"], requestValueDialog: IntentDialog? = nil, inputConnectionBehavior: InputConnectionBehavior = .default)
  • ドキュメント

    A property wrapper that indicates the associated property is an input argument of the app intent.


三歩目: 出力を持つインテント

動画から音声を抽出するインテント

インテント(アクション)を実行 → 動画選択 → 選択した動画ファイルを入力 → 音声を抽出(バックグラウンド処理)
→ 音声ファイルを出力

inline

実装

struct ExtractAudioIntent: AppIntent {
    static var title: LocalizedStringResource = ...
    static var description = ...

    @Parameter(title: "Video File", supportedTypeIdentifiers: ["public.movie"])
    var video: IntentFile

    // IntentFile型を返すことを明示
    func perform() async throws -> some IntentResult & ReturnsValue<IntentFile> {
        guard let videoURL = video.fileURL else { ... }
        
        let audioFileUrl: URL = ...
        // 動画から音声データを抽出して audioFileUrl に保存
        ...

        // 音声ファイルを `IntentFile` として出力
        let intentFile = IntentFile(fileURL: audioFileUrl)
        return .result(value: intentFile)
    }
}

ポイント1: ReturnsValue の型をちゃんと指定する

  • before
func perform() async throws -> some IntentResult

→ エラーになる:

Fatal error: perform() returned types not declared in method signature

  • after
func perform() async throws -> some IntentResult & ReturnsValue<IntentFile>

→ OK

perform() メソッドのと ReturnsValue の定義
  • perform() メソッドの定義: 戻り値の型は PerformResult
func perform() async throws -> Self.PerformResult
  • PerformResultIntentResultassociatedtype
associatedtype PerformResult : IntentResult
  • ReturnsValueIntentResult に準拠
public protocol ReturnsValue<Value> : IntentResult

ポイント2: IntentFile 型でファイル出力

ReturnsValue<URL> でファイルURLを出力しても、そのファイルを後段のインテント(アクション)で利用できない

func perform() async throws -> some IntentResult & ReturnsValue<URL> {
    ...
    return .result(value: audioFileUrl)
}

ReturnsValue<IntentFile> で、ファイルを出力する

func perform() async throws -> some IntentResult & ReturnsValue<IntentFile> {
    ...
    let intentFile = IntentFile(fileURL: audioFileUrl)
    return .result(value: intentFile)
}

→ 後段のアクション(例:サウンドを再生 / Play sound)で、抽出した音声ファイルを利用可能に


まとめ

  • 一歩目:アプリを起動するだけのインテント
    • 既存実装に手をいれる必要がない
    • 実質5行程度
  • 二歩目:入力を持つインテント
    • @Parameter プロパティラッパー
  • 三歩目:出力を持つインテント
    • ReturnsValue で出力する型を指定する
    • IntentFile 型でファイル出力

おわりに

本記事で取り上げているApp Intentsは、Chopper というiOSアプリをインストールすることで実際に試せます。


人の声だけを抽出して無音区間を除去するアプリ、Chopper

脚注
  1. 一歩目だけだとその先に困る、三歩ぐらいやるとユーザー体験として意味のあるインテントがつくれるようになる) ↩︎

Discussion