Creating Shortcuts with App Intents

Learn how to create iOS shortcuts using Swift in this App Intents tutorial. By Mark Struzinski.

5 (2) · 1 Review

Download materials
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

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:

Custom Confirmation View

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:

  1. Conforms to EntityQuery. This exposes the query to AppIntents.
  2. entities(for:) returns a list of entities matching the provided identifiers.
  3. 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:

  1. typeDisplayRepresentation returns a string that represents a title for the item.
  2. The id uniquely identifies this item. Aside from Int, you can also use UUID or String types for this property.
  3. displayRepresentation is the name of the value for this item. In the case of BreakLogger, this will be “15 Minutes”, “Half Hour”, or “One Hour”.
  4. defaultQuery defines the list of instances. You’re using the LogBreakQuery 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:

  1. If the LogBreakIntent has an entity already, use it to log a break.
  2. If it doesn’t already have a BreakIncrement, use the new parameter configuration you just created to call requestDisambiguation with a list of all entities and a message to explain what the user is selecting.
  3. Initialize the confirmation view with the selected BreakIncrement.
  4. 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:

Paramneter Selection

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:

Custom View with Parameter