Mastering App Intents: Querying Made Easy
🔍 Uncover the secrets of effective data retrieval in iOS development
App Intents are one of the most powerful features on iOS, but there is no clear pattern for sharing data and logic between your app and your App Intent. In this post, we will be building a simple to-do list to show how to share data between your app‘s existing data model and an App Intent. We will be focusing on a single item: marking a task in the list as complete. The GitHub repository of this project can be found here if you would like to code along!
Getting started
Let’s start by creating a new file in the Xcode project called CompleteTaskIntent
. In this file, create a struct
called CompleteTaskIntent
, and conform it to the AppIntents
protocol. For a more in-depth look into the components for setting up App Intents, you can visit my previous post.
Note: To conform to the AppIntents
protocol, you need to add a title and the perform method.
Your file should look similar to this:
If you run the above logic in Xcode, open the Shortcuts app, and tap the + button to add a new shortcut, you should see this intent displayed under the “Apps” section of actions to add to your shortcut.
Adding parameters
Now, let’s add our first property: the date.
You may notice that date
is a non-optional parameter. In this context, we are indicating to the system that this is a required parameter.
Now, lets add our second parameter: the task. Create a new parameter for your task like we did before — this time, the type of parameter is TodoTask
.
This code doesn‘t compile!
The system is not aware of our app’s data model, so this code does not compile. To expose this “custom data type” to the system, we must put it in a form the system can recognize. In the App Intents framework, the way we do this is to create a new model conforming to the AppEntity
.
An interface for exposing a custom type or app-specific concept to system services such as Siri and the Shortcuts app.
– Apple
Once we’ve conformed our model to this protocol, the code will compile.
Apple recommends creating another structure representing the parts of your data model that your intent will supply the information to.
For this exercise, we will extend our data model to conform to the AppEntity
protocol. You can add this code to a new file, or to the file containing the data model.
To conform to the AppEntity
protocol, we need to implement three properties:
displayRepresentation: DisplayRepresentation
: This property defines how the custom type will be displayed in the UI. You can specify thetitle
,subtitle
,synomyms
andimage
.defaultQuery: EntityQuery
: This provides the default query to be used when querying and retrieving entities with Shortcuts and Siri. When trying to resolve a parameter, the system will perform these queries to provide entities to be used by the UI.typeDisplayRepresentation: TypeDisplayRepresentation
: A short, localized, human-readable name for the type.
Creating a query
Let’s take a closer look at query creation. According to Apple‘s documentation, when the system needs to retrieve one or more specific instances of an app entity, it asks you to provide a relevant query type — these queries are used at parameter resolution times. When we create our queries, they must conform to the EntityQuery
protocol, which has two required methods illustrated below:
entities(for identifiers: [Entity.ID]) async throws -> [TodoTask]
: This method retrieves entities by their identifiers. Entities that do not match the identifiers will be skipped.func suggestedEntities() async throws -> [TodoTask]
: According to Apple, this method returns the initial results shown when a list of options backed by this query is presented.
Let’s continue — the aim of this intent is to provide a list of tasks for the user to choose from. Lets implement our query, starting with the suggestedEntities
method.
Currently, our query object has no notion of the selected date. In order to bridge this gap, we have the handy property wrapper @IntentParameterDependency
, which allows us to obtain the intent and its properties. It can be used on objects which conform to DynamicOptionsProvider
.
Add the code below to the TaskQuery section above, and build:
For our querying logic within the suggestedEntites()
method above, we ensure the selected date is present. If it isn’t present, we return an empty array. We then get the start of the selected date, and create the end date by adding one day to that. With this logic, we are trying to set up the window that queried tasks with fall within. Then, we also make sure the tasks returned have not already been completed, ensuring we have the most accurate data presented to the user. We then update the entities(for identifiers: [UUID])
method. In this method, we use the suggestedEntites
to return the tasks that match the identifiers provided by the system.
Adding a task
At this point in the process, we haven’t created any tasks, so when we run the app or try to execute the intent, we won’t see any options provided.
Let’s change that! Run the app and tap on the plus button on the main screen. A modal view will present itself — add a task there. You should see the task presented on the list once added.
Now that we’ve added a task, let’s execute our intent. Open the Shortcuts app and follow the video below to execute the intent:
You may have noticed that tapping the option in the presented menu does not mark the task as complete in the app. If you take a look in the CompleteTaskIntent.swift
file within the perform method, you‘ll see we haven’t added any logic to be executed after the parameter resolution phase has taken place.
In the Tasks
folder, there is a file called TaskManager
that has the ability to add a task to the database. Add this as a property to this object. We will create a new instance of this object within our intent — in the context of Shortcuts and Siri, there is no way for us to inject it as a dependency. (In other contexts, such as within the widget or in the main application, we may be able to inject the TaskManager
as a dependency.) The intent file should now look like this.
Now, when you execute the shortcut and select a task once you return to the app, that task will be completed. To make our user experience a little nicer, lets add a view to confirm that the task has been marked completed.
To achieve this, lets create our confirmation view by adding this code to the intent file:
To ensure our intent doesn’t crash, we will update our perform method return type to include ShowsSnippetView
:
Now we can call the result method and pass the successConfirmationView
as a parameter.
The app can now open once the task is executed. By default, the value of openAppWhenRun
is false
— if you’d like to open the app after the task has been executed, update this value to true
.
Wrapping up
In this blog post, we’ve explored the intricacies of creating an App Intent for our simple to-do list application. Starting with the basics, we delved into how to define and handle parameters such as the date and task selection, and then integrated them into our app‘s data model. Our journey included conforming to the AppIntents
protocol, dealing with the nuances of custom data types, and understanding the importance of entity queries in the AppIntents
framework.
As we have seen, integrating App Intents requires a thoughtful approach to app architecture and a deep understanding of both the app‘s data model and the AppIntents
framework. However, the payoff is a more streamlined and efficient user experience.
For those who are looking to further explore this topic or apply these concepts in their own projects, I encourage you to experiment with different types of intents and explore how they can be tailored to fit the unique requirements of your app. The possibilities are endless!