Creating Shortcuts with App Intents
Learn how to create iOS shortcuts using Swift in this App Intents tutorial. By Mark Struzinski.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Creating Shortcuts with App Intents
25 mins
Creating Custom Dialogs
If you want to show some custom UI instead of a default dialog, you can create your own in SwiftUI. BreakLoggedView.swift is already included in the starter project for this purpose. Open that file, and you’ll see it’s a small view meant to display the result of a log item. The break increments and messages are hard-coded right now, but you’ll make it more dynamic later.
Custom views meant for use in App Intents follow the same rules as widgets. They can’t show any animations or have any interactivity built in. They are for display only.
There are 3 places you can display custom views in the app intent lifecycle:
- Value confirmation
- Confirm a value the user has selected as part of a shortcut.
- Intent confirmation
- Confirm an intent before it runs.
- Completion
- Confirm that an intent has run.
Back in Xcode, open LogBreakIntent
. Update the perform()
function with the following signature:
func perform() async throws -> some ProvidesDialog & ShowsSnippetView
This lets App Intents know you’re providing custom UI as part of the result of the action.
Next, update the return statement to include the optional view
argument:
return .result(
dialog: "Logged a 15 minute break",
view: BreakLoggedView()
)
This provides BreakLoggedView
as part of the intent result for iOS to display.
Build and run. Trigger your shortcut from the Shortcuts app, and now you’ll see the custom view as confirmation when the intent completes:
Next, you’ll learn how to add parameters to your intents. This will allow users to choose from a list of break increment values instead of having a hard-coded 15 minute break.
Adding Parameters
First, open LoggerManager.swift and add a convenience function under logBreak(for:)
that will return all possible entries for a parameter:
static func allBreakIncrements() -> [BreakIncrementEntity] {
let entities = BreakIncrement.allCases.map { increment in
BreakIncrementEntity(
id: increment.rawValue,
name: increment.displayName
)
}
return entities
}
This function takes all cases of the BreakIncrement
enum and returns them in an array of BreakIncrementEntity
objects. You’ll create BreakIncrementEntity
below.
Next, create a new file named LogBreakQuery.swift in the Intents group. Add the following code:
import AppIntents
// 1
struct LogBreakQuery: EntityQuery {
// 2
func entities(for identifiers: [Int]) async throws -> [BreakIncrementEntity] {
// 3
let increments = LoggerManager.allBreakIncrements()
.filter { increment in
return identifiers.contains(increment.id)
}
return increments
}
}
An EntityQuery
lets the AppIntents
framework look up entities based on their identifier. For BreakLogger, LogBreakQuery
will perform this work.
The code above performs the following:
- Conforms to
EntityQuery
. This exposes the query toAppIntents
. -
entities(for:)
returns a list of entities matching the provided identifiers. - Uses
LoggerManager
to find entities based on their ID and returns them in an array.
Next, create a new file named BreakIncrementEntity.swift in the Intents group. Add the following code:
import AppIntents
struct BreakIncrementEntity: AppEntity {
// 1
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Break Increment"
// 2
let id: Int
let name: String
// 3
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(
title: LocalizedStringResource(
stringLiteral: name
)
)
}
var breakIncrement: BreakIncrement {
guard let increment = BreakIncrement(rawValue: id) else {
return .quarterHour
}
return increment
}
// 4
static var defaultQuery = LogBreakQuery()
}
The AppEntity
protocol defines a single item that can expose its properties to the App Intents system. By creating this type, your break increments are now visible as parameters to Shortcuts.
The code above does the following:
-
typeDisplayRepresentation
returns a string that represents a title for the item. - The id uniquely identifies this item. Aside from
Int
, you can also useUUID
orString
types for this property. -
displayRepresentation
is the name of the value for this item. In the case ofBreakLogger
, this will be “15 Minutes”, “Half Hour”, or “One Hour”. -
defaultQuery
defines the list of instances. You’re using theLogBreakQuery
you just created above for this purpose.
Now, add the prompt for a parameter to your intent. Open LogBreakIntent.swift and add a parameter at the top of the file underneath title
:
@Parameter(title: "Break Increment")
var breakIncrement: BreakIncrementEntity?
This exposes a parameter to App Intents named Break Increment. It uses the BreakIncrementEntity
you just created as its type.
Next, update BreakLoggedView
to accept a BreakIncrement
parameter. First, add a property to initialize it with:
struct BreakLoggedView: View {
let breakIncrement: BreakIncrement
// ...
}
And update the two Text
elements inside the view the to use the new property:
HStack {
Image(systemName: "clock")
.foregroundColor(Color("orange FF5A00"))
.padding(5)
Text("\(breakIncrement.displayName) Break Logged")
}
// ...
Spacer()
Text("\(breakIncrement.rawValue)")
.padding()
// ...
Update the preview provider to pass a break increment as well:
BreakLoggedView(breakIncrement: .halfHour)
.previewLayout(.sizeThatFits)
Next, in LogBreakIntent.swift, update perform()
with this code:
func perform() async throws -> some ProvidesDialog & ShowsSnippetView {
let loggerManager = LoggerManager()
let entity: BreakIncrementEntity
// 1
if let incrementEntity = self.breakIncrement {
loggerManager.logBreak(
for: incrementEntity.breakIncrement
)
entity = incrementEntity
} else {
// 2
let incrementEntity = try await $breakIncrement.requestDisambiguation(
among: LoggerManager.allBreakIncrements(),
dialog: IntentDialog("Select a break length")
)
entity = incrementEntity
loggerManager.logBreak(
for: incrementEntity.breakIncrement
)
}
// 3
let loggedView = BreakLoggedView(
breakIncrement: entity.breakIncrement
)
// 4
let logAmount = "\(entity.breakIncrement.rawValue)"
return .result(
dialog: "Logged a \(logAmount) minute break",
view: BreakLoggedView(
breakIncrement: entity.breakIncrement
)
)
}
Here’s what this does:
- If the
LogBreakIntent
has an entity already, use it to log a break. - If it doesn’t already have a
BreakIncrement
, use the new parameter configuration you just created to callrequestDisambiguation
with a list of all entities and a message to explain what the user is selecting. - Initialize the confirmation view with the selected
BreakIncrement
. - Update the return result to account for a different break length than 15 minutes.
Build and run. Close BreakLogger and run the shortcut to log a break. You’ll see the disambiguation prompt asking you to select your break length:
After you select a value, you’ll see the updated confirmation prompt. Both the text and the image representing the break length will update based on the value you selected from the list: