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
- 1 type of scenario works: if the bundle (display) name of the app is identical to what I say, e.g. "Next", Siri will properly open the app and initiate the call code, fantastic!
- However, the app bundle name must remain Nexxt.
- If I launch the app with the app name Nexxt and say "Call Emily using the Next app", Siri will respond with:
I don't see a "Emily" in your Contacts.
- I have added alternative app names, namely CFBundleSpokenName, aka Accessibility Bundle Name and INAlternativeAppNames:
- Next
- Dog
- Cat
- The alternative app names work! If I say "Open the Next app", "Open the Dog app" or "Open the Cat app" even when the actual app name is Nexxt, the app will be opened.
- However, if I say "Call Emily using the Dog app", Siri will respond with:
I don't see an app for that. You'll need to download one. Search the App Store
- I can't seem to make it work for my specific goal. Considering the alternative app names work, I figured something may be fundamentally wrong or missing in my "INStartCallIntent" implementation.
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!