Customizing an App Intent

Customizing an App Intent

Learn how to further customize intents for your SwiftUI apps with parameters and dialogs.

When creating an intent, a particular app feature is exposed to the system, allowing the user to invoke that capability via Shortcuts or Siri. The AppIntents framework is designed to personalize the device and allow users to perform these tasks while reducing most of the possible user experience frictions, achieving the goal in a simplified and standardized way. However, simplification doesn’t mean you can't personalize the intent experience.

In this tutorial, we are going to explore what customization means when talking about intents; in particular, we are going to discuss how to:

  1. Customize the text you want the system to display, or speak when requesting a value;
  2. Ask if the user wants to add optional parameters without automatically skipping them;
  3. Provide a dialog for the result of the intent.

Before we start

To address the topic we are going to leverage a simple sample project: a book library app that lets you keep track of your books on your shelf.

This starter project contains:

  • View folder:
    • BooksListView - shows the stored books as a list
    • BookDetailView - presents the details of a stored book
    • AddBookView - handles the addition of a new one
    • WebView - rendering the file
  • Model folder, collecting all the model files
    • Book - defines the book type
    • DataModel - data persisted using SwiftData
  • Manager folder
    • NavigationManager - handles navigation within the app
    • DataManager - handles the data operations
  • The BooksShelfCustomIntentApp file - the entry point of the app
  • Intents folder - collecting all files related to intents
    • BookEntity - handling the AppEntity for the Book model
    • OpenBookIntent - handling the AppIntent that allows opening a specific book
    • ShorcutsProviders - handling the AppShortcutsProvider conforming type that enables the invocation of the intents via vocal commands
    • SearchIntent - handling the AppIntent that allows to perform research inside your app
    • AddBookIntent - handling the AppIntent that creates a new instance of a book

The NavigationManager and the shared ModelContainer are initialized at the app entry point. Launch the app and start storing your books; they will be needed to launch and test.

Customize the text you want the system to display, or speak, when requesting a value

To customize the text you want the system to display, or speak, when requesting a value, we need to use the requestValueDialog parameter, an IntentDialog type, which asks the user for a value, allowing them to specify the words to prompt them with.

Step 1 - Implement the Request Value Dialog

import AppIntents

struct AddBookIntent: AppIntent {
    
    ...
    
    // 1. Add requestValueDialog for each parameter
    @Parameter(title: "Title", requestValueDialog: IntentDialog("What's the title?"))
    var title: String
    
    @Parameter(title: "Author", requestValueDialog: IntentDialog("Who's the author?"))
    var author: String
    
    @Parameter(title: "Genre", requestValueDialog: IntentDialog("What is the genre?"))
    var genre: Genre
    
    @Parameter(title: "Type of book", requestValueDialog: IntentDialog("Is it an e-book or a physical one?"))
    var type: BookContentType?
    
    ...
    
}
  1. Add the parameter requestValueDialog in the @Parameter initializer of each property with which you want to personalize the interaction.

Step 2 - Add the AppShortcut and test it

In the BookShelfShortcutsProvider:

import AppIntents

struct BookShelfShortcutsProvider: AppShortcutsProvider {
    
    static var appShortcuts: [AppShortcut] {
        
        ...
        
        // 1.
        AppShortcut(
            intent: AddBookIntent(),
            phrases: [
                "Add a new book in \(.applicationName)",
                "Add a book on my \(.applicationName)",
                "Put a book on the \(.applicationName)",
                "New book on my \(.applicationName)"
            ],
            shortTitle: "Add a book",
            systemImageName: "book"
        )
    }
}

  1. Adds a shortcut to launch the intent via vocal command.
If, when testing via vocal command, Siri is not speaking the dialogs, check Siri Responses in Siri settings and force them on Prefer Spoken Responses. It should then work.
0:00
/0:23
It can also be tested via Shortcuts app: when launching the intent, the dialog text displaying the parameter will adapt to the provided text.
0:00
/0:27

Ask if the user wants to add optional parameters without automatically skipping them

While testing the intent, you may have noticed that Siri doesn’t prompt the user for the type of book handled by the optional BookContentType parameter, as optional parameters are skipped by default.

However, if you want to customize the intent user experience and let the user choose whether to add optional parameters, you can add an extra parameter to handle this choice.

This parameter will ask the user whether they want to include the optional ones or not and, based on that choice, the intent will proceed with extra prompts to add such information or move on and treat it as nil.

Step 1 - Create an enum to handle the yes/no choice

To create such logic, we need an enumeration handling possible answers to the user's choice about whether to add or not the optional parameter.

enum IsTypeIncluded: String, CaseIterable, Codable, Identifiable {
    
    var id: Self { self }
    
    case yes = "Yes"
    case no = "No"
    
}

Step 2 - Conform it to AppEnum

This enum needs then to conform to AppEnum to be correctly handled and displayed in the intent interface.

enum IsTypeIncluded: String, CaseIterable, Codable, Identifiable, AppEnum {
    
    var id: Self { self }
    
    case yes = "Yes"
    case no = "No"

    static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Type Included")
    
    static var caseDisplayRepresentations: [Self: DisplayRepresentation] = [
        .yes: DisplayRepresentation(title: "Yes"),
        .no: DisplayRepresentation(title: "No")
    ]
}

Step 3 - Add the new parameter to the intent

Now that we have a fully working custom type being able to handle the user choices, in the AddBookIntent type add the following:

struct AddBookIntent: AppIntent {
    
    ...
    
    // 1.
    @Parameter(
        title: "Include Type",
        requestValueDialog: IntentDialog("Do you want to include the type of book?")
    )
    var isTypeIncluded: IsTypeIncluded
    
    ...
}
  1. Add a new parameter to handle the yes/no choice.

Step 4 - Implement the logic in the perform task

struct AddBookIntent: AppIntent {
    
    ...
    
    @MainActor
    func perform() async throws -> some IntentResult {
        // 1.
        var chosenType: BookContentType?
        
        // 2.
        if isTypeIncluded == IsTypeIncluded.yes {
            // 3.
            chosenType = try await $type.requestDisambiguation(
                among: BookContentType.allCases,
                dialog: "Which type?"
            )
        }
  
        ...
    }
}

  1. Declare a variable of type BookContentType.
  2. Check the choice handling parameter.
  3. Based on the user’s choice, when they want to add the additional parameter, request the value for the optional parameter by using requestDisambiguation(among:dialog:) on it. This method requests the user to disambiguate amongst an array of values for the parameter it is called on.
0:00
/0:31

Provide a dialog and a ShowsSnippetView for the result of the intent

When the intent is successfully accomplished, it is possible to provide the user with a view that can be fully customized.

Step 1 - Create a view displaying the outcome of the intent

import SwiftUI

struct IntentSnippetResultView: View {

    var book: Book
    
    var body: some View {
        HStack {
            Image(systemName: "books.vertical.fill")
                .imageScale(.large)
                .foregroundStyle(.tint)
                
            VStack(alignment: .leading) {
                Text("You have a new book on your shelf!")
                
                Divider()
                
                Section {
                    Text("Title: \(book.title)")
                    Text("Author: \(book.author!)")
                }
                
                Section {
                    Text("Genre: \(book.genre!.rawValue)")
                    Text("Type: \(book.contentType!.rawValue)")
                }
            }
            
        }
        .padding()
    }
}

Create a view to be displayed when the intent is successfully performed.

Step 2 - Return and show the view

In the AddBookIntent :

import AppIntents 
import SwiftUI

struct AddBookIntent: AppIntent {
    
    ...
    
    @MainActor
    // 1.
    func perform() async throws -> some IntentResult & ShowsSnippetView {
        
        ...
        
        // 2.
        return .result() {
            // 3.
            IntentSnippetResultView(book: newBook)
        }
    }
    
}
  1. Update the returning types of the perform method: IntentResult & ShowsSnippetView
  2. Return the entity value using the result(value:) method.
  3. In the closure also return the view to display.

Step 3 - Return a dialog

It may happen that you also want to return a specific text to be displayed. In the AddBookIntent add the following:

import AppIntents
import SwiftUI

struct AddBookIntent: AppIntent {
    
    ...
    
    @MainActor
    // 1.
    func perform() async throws -> some IntentResult & ShowsSnippetView & ProvidesDialog {
    
        ...
        
        // 2.
        let dialog = IntentDialog("New book added on your shelf.")
        
        // 3.
        return .result(dialog: dialog) {
            IntentSnippetResultView(book: newBook)
        }
    }
}
  1. Update the returning types of the perform method by adding ProvidesDialog type
  2. Create the dialog to display.
  3. Return the dialog in the result.

Now, when the intent is successfully performed, it won’t show done but the IntentDialog provided.

0:00
/0:35

Final Result

To recap, we have explored how to:

  • Personalize the text the system displays, or speaks, when requesting a value
  • Handle the optional parameters rather than let the system automatically skip them, and;
  • Provide a customized view and a dialog when the intent is successfully performed.

You can download the final project in the following link:

By customizing prompt dialogs using IntentDialog, including optional parameters through user-driven choices, and presenting result views with ShowsSnippetView and ProvidesDialog, you can tailor user experience making it more engaging and efficient.

These enhancements smooth user interactions, providing clarity and feedback to guide users effortlessly and confidently through tasks. As users interact with your app, they benefit from a more conversational and responsive interface, reflecting thoughtful design and attention to detail.