iosswiftvoipshortcutsiri

iOS Swift 5 - How to implement Siri VoIP command "Call Person using the Example app" support without having to add shortcuts?


Goal

I want to be able to trigger a VoIP call with Siri saying "Call Emily using the Next app", without adding shortcuts.

Emily is a contact I added to my contacts, which holds my own phone number. I test the example app on my daily driver phone, so it has a SIM card for calling.

Issue breakdown

I don't see a "Emily" in your Contacts.

I don't see an app for that. You'll need to download one. Search the App Store

Note, if you have trouble with changing the Display Name. Select project, change the display name, then click away from the project (to any file) and select the project again. Run the app and the name should update.

Code

Here's the code. It works for "Call Emily using the Next app" if my Display Name is Next. It also works for "Call Emily using the Dog app" if my Display Name is Dog.

The example app is written in SwiftUI code with a minimal setup to test the Siri feature.

TestSiriSimple -> TestSiriSimpleIntents -> IntentHandler:

import Intents

class IntentHandler: INExtension {
    
    override func handler(for intent: INIntent) -> Any {
        if intent is INStartCallIntent {
            return StartCallIntentHandler()
        }

        return self
    }
}

TestSiriSimple -> Shared -> StartCallIntentHandler:

import Foundation
import Intents

class StartCallIntentHandler: NSObject, INStartCallIntentHandling {
    
    func confirm(intent: INStartCallIntent) async -> INStartCallIntentResponse {
        let userActivity = NSUserActivity(activityType: String(describing: INStartCallIntent.self))
        return INStartCallIntentResponse(code: .continueInApp, userActivity: userActivity)
    }

    func handle(intent: INStartCallIntent, completion: @escaping (INStartCallIntentResponse) -> Void) {
        let response: INStartCallIntentResponse
        defer {
            completion(response)
        }
        
        let userActivity = NSUserActivity(activityType: String(describing: INStartCallIntent.self))
        response = INStartCallIntentResponse(code: .continueInApp, userActivity: userActivity)
        completion(response)
    }
    
    func resolveContacts(for intent: INStartCallIntent) async -> [INStartCallContactResolutionResult] {
        guard let contacts = intent.contacts, contacts.count > 0 else {
            return []
        }
        
        return [INStartCallContactResolutionResult.success(with: contacts[0])]
    }
    
    func resolveCallCapability(for intent: INStartCallIntent) async -> INStartCallCallCapabilityResolutionResult {
        INStartCallCallCapabilityResolutionResult(callCapabilityResolutionResult: .success(with: intent.callCapability))
    }
    
    func resolveDestinationType(for intent: INStartCallIntent) async -> INCallDestinationTypeResolutionResult {
        INCallDestinationTypeResolutionResult.success(with: .normal)
    }
}

The root app class is unchanged. TestSiriSimple -> Shared -> ContentView:

import SwiftUI
import Intents

struct ContentView: View {
    @State private var status: INSiriAuthorizationStatus = .notDetermined
    
    var body: some View {
        Text("Hello, world! Siri status: \(status.readableDescription)")
            .padding()
            .onAppear {
                requestSiri()
            }
            .onContinueUserActivity(NSStringFromClass(INStartCallIntent.self)) { userActivity in
                continueUserActivity(userActivity)
            }
    }
    
    private func requestSiri() {
        INPreferences.requestSiriAuthorization { status in
            self.status = status
        }
    }
    
    private func continueUserActivity(_ userActivity: NSUserActivity) {
        if let intent = userActivity.interaction?.intent as? INStartCallIntent {
            // Find person from contacts or create INPerson from app specific contacts.
            // Execute VoIP code.
            // I consider it a success if Siri responds with "Calling Now", opens the app and reaches this code.
        }
    }
}

extension INSiriAuthorizationStatus {
    var readableDescription: String {
        switch self {
        case .authorized:
            return "Authorized"
        case .denied:
            return "Denied"
        case .notDetermined:
            return "Not determined"
        case .restricted:
            return "Restricted"
        default:
            return "Unknown"
        }
    }
}
Details

TestSiriSimple -> (Main) Info.plist

TestSiriSimpleIntents -> Info.plist

Privacy - Siri Usage Description = Siri wants to let you start calls in this app.

TestSiriSimpleIntents target has INStartCallIntent as a supported intent

If you have any ideas, they are more than welcome!

I'm willing to share a zip of my example code if you could show me how I would go about that in StackOverflow. If any other other info would help, don't hesitate to comment!


Solution

  • There's a better phrase than Call Emily using the Nexxt app, try the following:

    Call Emily on Nexxt
    

    See documentation: https://developer.apple.com/documentation/sirikit/instartaudiocallintent

    Example phrase