iosswiftsirisirishortcuts

How to manage that Siri intent launches my app and transfers data to the app?


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" enter image description here

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.


Solution

  • 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:

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