Siri Shortcuts on Apple Watch

Learn how to take advantage of Siri and Shortcuts on the Apple Watch without any intervention required from the iOS companion app. By Mark Struzinski.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Defining Suggestions

Finally, in the Suggestions section, set default image to rw-logo and set Summary to Hydration type and amount. These sections determine the prompt users will see when configuring your shortcut from a Siri suggestion.

Setting Intent Suggestions

Defining Siri’s Response

The response node defines how Siri will communicate the result of the shortcut back to the user.

  1. Select Response on the left sidebar.
  2. Under success, enter Item Added! for both fields: Voice-Only Dialog and Printed Dialog.
  3. Under failure, enter Sorry, but I couldn’t add your item. for both fields.

Adding the Intents Extensions

Next, you need to add an extension for the iOS and watchOS platforms. The extensions allow shortcuts to run outside of the app context. The app doesn’t have to be in the foreground for the shortcut to run when the extension is present. Intent extensions enable a lot of different types of interactions. Your users can add actions to shortcuts as part of a large, complex shortcut or as one-offs that just perform a single task. Now, your shortcut actions are available from the Shortcuts app or by voice via Siri.

Selecting Intent Extension

Intent Dialog

WatchOS Intent Extension

  1. In Xcode, select File ▸ New ▸ Target…
  2. Select iOS at the top for the platform.
  3. Under the Application Extension category, select Intents Extension.
  4. Click Next.
  5. On the next screen, set Product Name to HydratorIntents.
  6. Select your team from the Team drop-down.
  7. Set Starting Point to None. Include UI Extension isn’t checked, Project is Hydrator and Embed in Application is set to Hydrator.
  8. Click Finish.
  9. Now, repeat that process, but select watchOS for the platform and name the extension HydratorIntentsWatch.
  10. Ensure the Embed in drop-down is set to HydratorWatch Extension.

New target options

Adding Framework Dependencies

Each extension will depend on an underlying framework for data management. You’ll add those now.

HydratorKit

  • For HydratorIntents, add HydratorKit.
  • For HydratorIntentsWatch, add HydratorKitWatchOS.
  1. Select each new target in Xcode.
  2. In Frameworks and Libraries, click + to add the relevant HydratorKit framework.

HydratorKit Watch

Configuring for Builds

You need to perform some maintenance work to make the new extensions work with the .xcconfig files you learned about earlier.

XCConfig Setup

  1. In Project navigator, select the Hydrator project node.
  2. In Projects, select Hydrator.
  3. Select the Info tab.
  4. Under Configurations, in the middle pane, expand Debug and Release.
  5. For HydratorIntents and HydratorIntentsWatch, select the appropriate .xcconfig file from the drop-down menu.

Configuring Identifiers

The data storage for the extensions uses app groups to write to a shared location. You need to set up app groups for the new targets for this to work.

  1. In Project navigator, select the Hydrator project node.
  2. Select the HydratorIntents target.
  3. Select the Signing & Capabilities tab on top.
  4. Click + Capability.
    Add Capability
  5. In the menu, select App Groups and click Enter.
  6. Repeat this process for the HydratorIntentsWatch target.
  7. This process generates an entitlements file inside each respective extension group.

Enabling Entitlements Files

For each entitlements file, perform the following steps:

Adding group ID to the entitlements files

  1. Select the entitlements file in Project navigator.
  2. Expand the App Groups node.
  3. Click + to add a new entry to the array.
  4. In the Value column, add the variable $(GROUP_ID).

Now your Group ID maps to the variable in the .xcconfig files.

Updating Info.plist Files

Next, you need to update the Info.plist files in the new targets to declare support for the intent.

In each plist for the new targets, do the following:

  1. Open NSExtension.
  2. Next, open NSExtensionAttributes.
  3. Then, open IntentsSupported.
  4. Click + to add a new array item.
  5. Add TrackIntakeIntent as the value for the new item.

Plist Intent Support

This step allows the system to invoke the right class when Siri uses the intent. Next, you’ll move on to code generation for your intent and creating a shared handler.

Generating Intent Code

Now, you need to set up code generation options for the intent. Xcode can create the correct classes for you based on your intent setup. You’ll interact with these generated classes when you create the shared intent handler.

Set up the following:

  • For the Hydrator, HydratorWatchExtension, HydratorIntents and HydratorIntentsWatch targets, ensure No Generated Classes is selected.
  • HydratorWatch won’t allow any changes and is set to No Role.
  • For HydratorKit and HydratorKitWatchOS, select Public Intent Classes.
  1. Select the intent definition file in Project navigator following the path: Hydrator ▸ Shared ▸ Resources.
  2. Open File inspector by pressing Command-Option-1.
  3. Under Target Membership, select all targets.
  4. The property to the right of each target is the code generation scheme.

The code generation for each target

Sharing Intent Handling

You’ll set up both intent extensions to use the same code to centralize intent handling in one place. You can now use the types generated from your intent definition file.

  1. Navigate to the Shared group in Project navigator.
  2. Open Source inside it.
  3. Create a new group named Intents.
  4. Create a new Swift file named TrackIntakeIntentHandler.swift.
  5. In Target Membership for this file, select HydratorIntents and HydratorIntentsWatch. Deselect all other targets.

Now you’re ready to set up your shared intent handling code. At the top of the file, add the following code:

#if os(iOS)
import HydratorKit
#else
import HydratorKitWatchOS
#endif

The compiler directive above selectively imports the correct framework depending on which platform is active.

Next, create the class declaration:

class TrackIntakeIntentHandler: NSObject, TrackIntakeIntentHandling {
}

You’ll get an error that the class does not conform to TrackIntakeIntentHandling. This is your first use of one of the generated classes resulting from your intent definition file. Command-click into the definition of this protocol to see the generated header, and you’ll see something like this:

@available(iOS 12.0, macOS 11.0, watchOS 5.0, iOS 12.0, *)
@available(tvOS, unavailable)
@objc(TrackIntakeIntentHandling) public protocol TrackIntakeIntentHandling : NSObjectProtocol {

    @objc(handleTrackIntake:completion:) func handle(intent: HydratorKit.TrackIntakeIntent, completion: @escaping (HydratorKit.TrackIntakeIntentResponse) -> Void)

    @available(iOS 13.0, macOS 11.0, watchOS 6.0, *)
    @objc(resolveOuncesForTrackIntake:withCompletion:) func resolveOunces(for intent: HydratorKit.TrackIntakeIntent, with completion: @escaping (HydratorKit.TrackIntakeOuncesResolutionResult) -> Void)

    @available(iOS 13.0, macOS 11.0, watchOS 6.0, *)
    @objc(resolveHydrationTypeForTrackIntake:withCompletion:) func resolveHydrationType(for intent: HydratorKit.TrackIntakeIntent, with completion: @escaping (HydratorKit.HydrationTypeResolutionResult) -> Void)

    @objc(confirmTrackIntake:completion:) optional func confirm(intent: HydratorKit.TrackIntakeIntent, completion: @escaping (HydratorKit.TrackIntakeIntentResponse) -> Void)
}

Back in TrackIntakeIntentHandler.swift, click Fix in the error message. Xcode will generate the appropriate method stubs for you.

Resolving Parameters

Inside resolveOunces(for:with:), add the following code:

// 1
guard
  let ounces = intent.ounces?.intValue,
  ounces != 0
else {
  let result = TrackIntakeOuncesResolutionResult.needsValue()
  completion(result)
  return
}

// 2
let result = TrackIntakeOuncesResolutionResult.success(with: ounces)
completion(result)

Here’s what you’re doing:

  1. Check for the ounces variable, cast to Int and make sure it isn’t zero. If any of these checks fail, return a needsValue() result, which will invoke the disambiguation response you set up in your intent handler.
  2. If all conditions are satisfied, return a success result.

Next, inside resolveHydrationType(for:with:), add the following code:

// 1
let hydrationValue = intent.hydrationType.rawValue
guard 
  let hydrationType = HydrationType(rawValue: hydrationValue), 
  hydrationType != .unknown 
else {
  let result = HydrationTypeResolutionResult.needsValue()
  completion(result)
  return
}

// 2
let result = HydrationTypeResolutionResult.success(with: hydrationType)
completion(result)

Here’s what you’re doing:

  1. Get rawValue from the enum passed in the arguments. Attempt to create HydrationType from Int and ensure it doesn’t come back .unknown. If any of these checks fail, return a needsValue() result, which will start the disambiguation flow.
  2. If all conditions are satisfied, return a success result.

Once you satisfy all those conditions, handle(intent:completion:) gets called.

Fill that method out now with the following code:

// 1
let hydrationValue = intent.hydrationType.rawValue
// 2
let drink = DrinkType(rawValue: hydrationValue)
// 3
guard 
  let drinkType = drink,
  let ounces = intent.ounces?.intValue 
else {
  let response = TrackIntakeIntentResponse(
    code: .failure,
    userActivity: nil)
  completion(response)
  return
}

// 4
let item = LogEntry(drinkType: drinkType, ounces: ounces)
let dataManager = DataManager()
dataManager.save(entry: item)

// 5
let response = TrackIntakeIntentResponse(
  code: .success,
  userActivity: nil)
completion(response)

Here’s what you’re doing above:

  1. Get the hydration value from the shortcut’s input.
  2. Create a DrinkType from the value.
  3. Check to ensure you have valid DrinkType and ounces input, otherwise call completion with a failure result.
  4. Create LogEntry and saving it to the data store.
  5. Call completion with a successful response.

Note that the completion handler argument is using the generated types from your intent definition file.