🍎

【Swift】App Intents を触ってみる

2024/12/16に公開

初めに

App Intents とは、ユーザーがアプリのタスクを素早く実行できるようにするフレームワークです。
App Inetnts は2022年のWWDCで発表され、2024年のWWDCでもアップデートが発表されていました。

システムの利便性を高めるApp Intentのデザイン の動画では、「App Intents contain all the tasks your app can do」という部分があり、 Apple Intelligence などと組み合わせて今後活発に使用されるようになることが予想されています。

今回は App Intents を触ってみて、簡単にどのようなことができるかまとめてみたいと思います。

記事の対象者

  • Swift, SwiftUI 学習者
  • App Intents に触れてみたい方

目的

今回の目的は先述の通り、App Intents に触れてみることです。
本来であればショートカットアプリで他のアプリと連携してタスクを実行することもできるのですが、今回は比較的単純なタスクを App Intents から呼び出すような実装を行います。

App Intents とは

App Intents について、公式ドキュメントの App Intents - Overview から引用します。

The App Intents framework provides functionality to deeply integrate your app’s actions and content with system experiences across platforms, including Siri, Spotlight, widgets, controls and more. With Apple Intelligence and enhancements to App Intents, Siri suggests your app’s actions to help people discover your app’s features and gains the ability to take actions in and across apps.

(日本語訳)
App Intentsフレームワークは、Siri、Spotlight、ウィジェット、コントロールなど、プラットフォーム全体のシステムエクスペリエンスとアプリのアクションやコンテンツを深く統合するための機能を提供します。AppleのインテリジェンスとApp Intentsの強化により、Siriはアプリのアクションを提案してアプリの機能を人々に発見させる手助けを行い、アプリ内およびアプリ間でアクションを実行できるようになります。

上記をまとめると以下のようなことが言えるかと思います。

  • App Intents を使えばプラットフォームのシステムとアプリを統合できる
  • Siri からアプリ内のアクションを呼び出すことができる

実装

実装は以下の手順で進めていこうと思います。

  1. シンプルな App Intents の実装
  2. アプリを開く App Intents の実装
  3. ダイアログを開く App Intents の実装
  4. タイマーの開始 / 終了ができる App Intents の実装

1. シンプルな App Intents の実装

まずは最もシンプルな形の App Intents を実装してみます。
なお、今回は全て実機で試していきます。

コードは以下の通りです。
AppIntent では titleperform を実装しています。
title は名前の通り AppIntent のタイトルであり、ショートカットアプリなどで表示される名前を設定できます。
perform ではその AppInetnt が呼び出された際に実行する内容を定義することができます。
上記のコードでは返り値を IntentResult として、単純に文字列を print して、空の result を返すようにしています。

import AppIntents

struct SimpleIntent: AppIntent {
    static let title: LocalizedStringResource = "Simple Intent"

    @MainActor
    func perform() async throws -> some IntentResult {
        print("Simple Intent performed !")
        return .result()
    }
}

この SimpleIntent を追加したアプリケーションを実行して、以下の動画のようにショートカットアプリを開き、 SimpleIntent を実行するとコンソールに「Simple Intent performed !」と表示されているかと思います。
これで AppIntent が正常に動作していることが確認できます。
見た目としては以下の動画のように変化がないことがわかります。

https://youtube.com/shorts/34eU8ld5SdQ

2. アプリを開く App Intents の実装

次に、アプリを開く AppIntent を実装していきます。
コードは以下の通りです。
基本的には先程の実装と同様ですが、 openAppWhenRun というパラメータを true にしています。このようにすることで、名前の通りこの OpenAppIntent が呼び出された際にはアプリが起動するようになります。

import AppIntents

struct OpenAppIntent: AppIntent {
    static let title: LocalizedStringResource = "Open App Intent"
    static var openAppWhenRun: Bool = true
    
    @MainActor
    func perform() async throws -> some IntentResult {
        print("Open App Intent performed !")
        return .result()
    }
}

これで実行すると、以下の動画のように App Intents が実行されるとアプリが開くことが確認できます。
https://youtube.com/shorts/YeWSX76XfSI

3. ダイアログを開く App Intents の実装

次にダイアログを開く AppIntent を実装していきます。
コードは以下の通りです。

以下では返り値を ReturnsValue<String> & ProvidesDialog のようにしています。これで、 result として valuedialog を返すことができます。

value ではこの ShowDialogAppIntent が実行された際にアプリ側に渡したい値を設定することができます。この実装では特に渡す値はないためからの文字列にしています。

dialog では IntentDialog を渡すことができ、そこに表示させたい文言を入れることで、ダイアログに文言を表示させることができるようになります。

import AppIntents

struct ShowDialogAppIntent: AppIntent {
    static let title: LocalizedStringResource = "Show Dialog Intent"
    static var openAppWhenRun: Bool = true

    @MainActor
    func perform() async throws -> some ReturnsValue<String> & ProvidesDialog {
        return .result(value: "", dialog: IntentDialog("Welcome back !"))
    }
}

上記のコードで実行すると、以下の動画のようにアプリが開き、それと同時にダイアログが表示されることがわかります。

https://youtube.com/shorts/Ovo1e1LQS0Y

4. タイマーの開始 / 終了ができる App Intents の実装

最後にアプリ内のタイマーを開始 / 終了できる AppIntent を実装していきます。

まずはアプリ内で実行できるタイマーを作成していきます。

タイムを Manager で管理するために TimerManager を作成します。
コードは以下です。
seconds で秒数を保持して、タイマーのスタート、ストップ、リセットができるようにしています。
また、 shared で外部から TimerManager を参照できるようにしています。

import Foundation

class TimerManager: ObservableObject {
    static let shared = TimerManager()
    @Published var seconds: Int = 0
    
    private var timer: Timer?
    private var startDate: Date?
    
    private init() {}
    
    func startTimer() {
        guard timer == nil else { return }
        startDate = Date()
        timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
            self?.seconds += 1
        }
    }
    
    func stopTimer() {
        timer?.invalidate()
        timer = nil
        startDate = nil
    }
    
    func resetTimer() {
        stopTimer()
        seconds = 0
    }
}

次にビューを作成していきます。
コードは以下の通りです。
TimerManager.sharedTimerManager にあるタイマーのスタート、ストップ、リセットを実行できるようにしています。

import SwiftUI

struct TimerView: View {
    @ObservedObject private var timerManager = TimerManager.shared

    var body: some View {
        VStack(spacing: 20) {
            Text("Timer")
                .font(.largeTitle)
                .padding()
            
            Text("\(timerManager.seconds) seconds")
                .font(.title)
                .foregroundColor(.green)
            
            HStack(spacing: 20) {
                Button(action: {
                    timerManager.startTimer()
                }) {
                    Text("Start")
                }
                .buttonStyle(.borderedProminent)
                
                Button(action: {
                    timerManager.stopTimer()
                }) {
                    Text("Stop")
                }
                .buttonStyle(.borderedProminent)
                
                Button(action: {
                    timerManager.resetTimer()
                }) {
                    Text("Reset")
                }
                .buttonStyle(.bordered)
            }
        }
        .padding()
    }
}

次にタイマーを開始する AppIntent を作ります。
コードは以下の通りです。

perform メソッドの中で TimerManager.shared.startTimer() を実行することで、タイマーをスタートしています。

StartTimerIntent.swift
import AppIntents

struct StartTimerIntent: AppIntent {
    static var title: LocalizedStringResource = "Start Timer"
    static var description = IntentDescription("Starts the timer.")
    static var openAppWhenRun: Bool = true
    
    func perform() async throws -> some IntentResult & ProvidesDialog {
        await MainActor.run {
            TimerManager.shared.startTimer()
        }
        return .result(dialog: "Timer started.")
    }
}

次にタイマーを終了する AppIntent を作ります。
コードは以下の通りです。

perform メソッドの中で TimerManager.shared.stopTimer() を実行することで、タイマーをストップしています。
また、タイマーをストップした段階で経過時間を音声で読み上げるようにしています。

今回はこれらの AppIntent をショートカットアプリから呼び出していますが、追加の対応を入れると Siri でも使用できるため、一連の処理を Siri で行えば、スマホを触ることなくアプリ内のタイマーの開始 / 終了ができ、終了時間も知ることができるようになります。

import AppIntents
import AVFoundation

struct StopTimerIntent: AppIntent {
    static var title: LocalizedStringResource = "Stop Timer"
    static var description = IntentDescription("Stops the timer.")
    static var openAppWhenRun: Bool = true
    
    func perform() async throws -> some IntentResult & ProvidesDialog {
        let elapsedSeconds: Int = await MainActor.run { () -> Int in
            TimerManager.shared.stopTimer()
            return TimerManager.shared.seconds
        }
        
        // 時間をフォーマット
        let formattedTime = formatTime(seconds: elapsedSeconds)
        
        let utterance = AVSpeechUtterance(string: "タイマーを停止しました。経過時間は \(formattedTime) です。お疲れ様でした。")
        // 喋る言語の設定
        utterance.voice = AVSpeechSynthesisVoice(language: "ja-JP")
        utterance.rate = 0.5

        let synthesizer = AVSpeechSynthesizer()
        synthesizer.speak(utterance)
        
        return .result(dialog: "タイマーを停止しました。経過時間は \(formattedTime) です。")
    }

    func formatTime(seconds: Int) -> String {
        let hours = seconds / 3600
        let minutes = (seconds % 3600) / 60
        let secs = seconds % 60

        var components: [String] = []
        if hours > 0 {
            components.append("\(hours)時間")
        }
        if minutes > 0 {
            components.append("\(minutes)分")
        }
        if secs > 0 || components.isEmpty {
            components.append("\(secs)秒")
        }
        return components.joined()
    }
}

上記のコードで実行すると以下のようにタイマーの開始 / 終了ができることがわかります。

https://youtube.com/shorts/Nn3gjMwIBDU

まとめ

最後まで読んでいただいてありがとうございました。

今回は App Intents について簡単に触れてみました。
今回は紹介できませんでしたが、AppShortcutsProvider に App Intents を登録することで Siri からも呼び出せるようになるため、非常に便利になるかと思います。

誤っている点やもっと良い書き方があればご指摘いただければ幸いです。

参考

https://zenn.dev/naoya_maeda/articles/bd6cc7d3cb6154

https://developer.apple.com/documentation/AppIntents

https://developer.apple.com/jp/news/?id=jdqxdx0y

https://developer.apple.com/jp/videos/play/wwdc2024/10176/

Discussion