After hours of searching I ask for your help.
I'm writing an app to manage my inventory. As an alternative to the manual typing in of the inventory items via a dedicated view "AddNewEntry" I would like to use Siri and an intent to catch the data in the sense of "Store item at place in section". item, place and section are the properties I want to get via Siri and then store them in my database using a view "AddNewEntry" where I handle the manual input, check and storage of these data. For the handover of the new data from the intent I want to post a notification to my mainViewController which then starts the segue to the View to show and check/store the new entry.
I have set up the intent ("VorratAdd") via intent definition and the shortcut seems to work fine and collects the data. But the shortcut doesn't return to my app and ends in a popup-view with the message like (my translation from German):
"Vorrat Add" couldn't be executed. Tasks takes too long to be closed. Retry again"
My set up is as follows:
handler for my intent VorratAdd:
import Foundation
import Intents
import UIKit
class VorratAddIntentHandler: NSObject, VorratAddIntentHandling {
func handle(intent: VorratAddIntent, completion: @escaping (VorratAddIntentResponse) -> Void) {
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "NewVorratInputFromSiri"), object: intent)
}
func resolveBezeichnung(for intent: VorratAddIntent, with completion: @escaping (INStringResolutionResult) -> Void) {
if intent.item == " " {
completion(INStringResolutionResult.needsValue())
} else {
completion(INStringResolutionResult.success(with: intent.bezeichnung ?? "_"))
}
}
func resolveOrt(for intent: VorratAddIntent, with completion: @escaping (INStringResolutionResult) -> Void) {
if intent.place == " " {
completion(INStringResolutionResult.needsValue())
} else {
completion(INStringResolutionResult.success(with: intent.ort ?? "_"))
}
}
func resolveFach(for intent: VorratAddIntent, with completion: @escaping (VorratAddFachResolutionResult) -> Void) {
if intent.section == 0 {
completion(VorratAddFachResolutionResult.needsValue())
} else {
completion(VorratAddFachResolutionResult.success(with: intent.fach as! Int))
}
}
}
IntentHandler.swift:
import UIKit
import Intents
class IntentHandler: INExtension {
override func handler(for intent: INIntent) -> Any {
guard intent is VorratAddIntent else {
fatalError("unhandled Intent error: \(intent)")
}
return VorratAddIntentHandler()
}
}
Donation in my mainViewController "HauptVC":
extension HauptVC {
func initSiriIntent() {
let intent = VorratAddIntent()
intent.suggestedInvocationPhrase = "Neuer Vorrat"
intent.item = " "
intent.location = " "
intent.section = 0
let interaction = INInteraction(intent: intent, response: nil)
interaction.donate { (error) in
if error != nil {
if let error = error as NSError? {
print("Interaction donation fehlgeschlagen: \(error.description)")
} else {
print("Interaction donation erfolgreich")
}
}
}
}
}
AppDelegate:
import UIKit
import Intents
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
var rootViewController: UINavigationController?
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
if userActivity.activityType == "VorratAddIntent" {
restorationHandler(rootViewController!.viewControllers)
return true
}
return false
}
SceneDelegate:
import UIKit
import Intents
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
let app = UIApplication.shared.delegate as! AppDelegate
var rootVC: UINavigationController?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let _ = (scene as? UIWindowScene) else { return }
rootVC = self.window?.rootViewController as? UINavigationController
app.rootViewController = rootVC
}
func scene(_ scene: UIScene, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
if userActivity.activityType == "VorratAddIntent" {
app.rootViewController = rootVC
restorationHandler(rootVC!.viewControllers)
return true
}
return false
}
Has anyone an idea what I'm doing wrong? Thank you so much for your support.
I think you are missing one method implementation "confirm"
func confirm(intent: VorratAddIntent, completion: @escaping (VorratAddIntentResponse) -> Void) {
completion(.init(code: .success, userActivity: nil))
}
Edit:
Here is documentation for confirming intent details
Edit 2:
You must call the completion handler in the "handle" method
func handle(intent: VorratAddIntent, completion: @escaping (VorratAddIntentResponse) -> Void) {
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "NewVorratInputFromSiri"), object: intent)
completion(your response)
}
Edit 3 - Passing data from extension(Siri) to app:
NotificationCenter does not work between extension and main app, because these are not connected in any way it is completely different process.
There are different ways to achieve this:
App Group
:
Here is example with Core Data and Extension how you can share the database
UserDefaults with AppGroup
:
You can also use UserDefaults(suiteName: your app group name)
NSUserActivity
:
Most convenient way for SiriKit apple doc
Example of NSUserActivity
You will create instance of NSUserActivity in handle method, like this:
func handle(intent: VorratAddIntent, completion: @escaping (VorratAddIntentResponse) -> Void) {
let response = VorratAddIntentResponse.success()
let activity = NSUserActivity(activityType:NSStringFromClass(VorratAddIntent.self))
activity.addUserInfoEntries(from: [
"task_id" : your task id
])
response.userActivity = activity
completion(response)
}
And than you can handle the user info in your app, if the app is already running you extract the task_id value like this:
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
guard userActivity.interaction?.intent is VorratAddIntent else { return }
#if DEBUG
print("Siri Intent user info: \(String(describing: userActivity.userInfo))")
#endif
guard let taskID = userActivity.userInfo?["task_id"] as? String
else { return }
}
But if your app is not running in background, you need to handle user activities in scene(willConnectTo:)
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let _ = (scene as? UIWindowScene) else { return }
for activity in connectionOptions.userActivities {
self.scene(scene, continue: activity)
}
}