Adding 3D Touch Quick Actions

Last updated: Jun 12, 2020

Apple gave us all a surprise when they announced the iPhone 6S and 6S Plus devices by adding 3D Touch. The taptic engine allows the device to sense the pressure of a touch and trigger actions. Pressing hard on the home screen icon can now launch a quick action menu to take the user directly to parts of the app. Within an app pressing hard on a view allows the user to peep or preview content and then pop to navigate to the content.

If you are updating your app to support iOS 9 you need to support these features. They are not required but we all know that users with devices that support 3D Touch will expect your app to do something. Imagine the disappointment when a user presses hard on your app icon and nothing happens.

The good news is that these are features that you can add quickly. In this post I will get started by adding some static home screen actions to my Stacks app that will allow the user to directly jump to one of the tab bar controller views.

Home Screen Quick Actions

Quick actions can be static or dynamic. As the name suggest you define static actions at build time and they are visible to the user when they install or update the app. You add dynamic actions at run time so the user will only see them once they have launched the app at least once. Either way you can only have four with priority going to the static actions.

Each quick action can have a title and optional subtitle and icon. The system also take care of VoiceOver support for you.

Adding the Shortcut Items

You add static home screen quick actions using the UIApplicationShortcutItems array in the app Info.plist file. Each entry in the array is a dictionary containing items matching properties of the UIApplicationShortcutItem class:

  • UIApplicationShortcutItemType A required app specific string used to identify the quick action. I am prefixing the action with the app bundle identifier (e.g. com.useyourloaf.stacks).

  • UIApplicationShortcutItemTitle A required string that is displayed to the user. Will display on two lines if necessary as long as you do not have a subtitle. Localize in InfoPlist.strings file.

  • UIApplicationShortcutItemSubtitle An optional string displayed below the title. Localize in InfoPlist.strings file.

  • UIApplicationShortcutItemIconType Optional string for a system icon to display with the quick action. See next item if you want a custom icon.

  • UIApplicationShortcutItemIconFile Optional string for an icon image in app bundle or asset catalog. Icon is a template file with size 35x35 points.

  • UIApplicationShortcutItemUserInfo Optional dictionary with any additional information you want to pass. This is a good place to include an app version number.

I have added a shortcut item for each tab in the tab bar controller. You can see the Info.plist keys for the first one below:

I have localized both the title and subtitle fields so my base InfoPlist.strings file contains:

shortcutTitleFavorites = "Favorites";
shortcutSubtitleFavorites = "What you like";

I have also added icon templates for each shortcut to the asset catalog. These icons are single colour with sizes 35x35 (1x), 70x70 (2x) and 105x105 (3x).

Finally I included the app bundle version in the user info dictionary. I am not bothering to check this when the quick action is used. It becomes useful in the future if the app changes in a way that means one of the actions must be handled differently.

You can see the appearance of the home screen quick action menu below:

Quick Action Menu

Swift enums are Good

We do not want lots of magic values in our code so we define our quick actions using an enum in our app delegate. If we use a String type the raw value for each member is the text of that member’s name so we make sure our names match the value we used in Info.plist.

enum ShortcutIdentifier: String {
  case OpenFavorites
  case OpenFeatured
  case OpenTopRated        

The shortcut item types I defined in Info.plist are app-specific strings prefixed by the application bundle identifier. To make it easy to match against our shorter enum member name we can use a custom initialiser that takes the full string, strips the prefix and then tries to initialise using the raw value:

  init?(fullIdentifier: String) {
    guard let shortIdentifier = fullIdentifier.componentsSeparatedByString(".").last else {
      return nil
    }
    self.init(rawValue: shortIdentifier)
  }

This initialiser either returns a valid ShortcutIdentifier or nil.

Handling the Shortcut

When a user chooses one of the quick actions the systems launches or resumes the app and calls the performActionForShortcutItem method in your app delegate:

func application(application: UIApplication,
     performActionForShortcutItem shortcutItem: UIApplicationShortcutItem,
     completionHandler: (Bool) -> Void) {

  completionHandler(handleShortcut(shortcutItem))
}

The system expects you to call the completion handler once you have handled the quick action indicating whether or not you succeeded. For reasons which become clear in a moment I keep the shortcut handling code in a separate private function which takes the shortcut item as an argument and returns a Bool indicating success:

private func handleShortcut(shortcutItem: UIApplicationShortcutItem) -> Bool {        
  let shortcutType = shortcutItem.type
  guard let shortcutIdentifier = ShortcutIdentifier(fullIdentifier: shortcutType) else {
    return false
  }  
  return selectTabBarItemForIdentifier(shortcutIdentifier)
}

This function first gets the shortcut item type and then uses our custom enum initialiser to try and get our ShortcutIdentifier. The guard protects us from failure and exits if we do not recognise the shortcut. I will skip the detail but when we have a valid shortcut we call selectTabBarItemForIdentifier to choose the right tab in the tab bar controller based on the shortcut identifier.

So Good They Called It Twice

There is one final tweak we need. When a quick action causes the app to launch the application delegate methods willFinishLaunchingWithOptions and didFinishLaunchingWithOptions get called before the performActionForShortcutItem method. When a quick action causes the app to resume the system only calls the performActionForShortcutItem method.

To avoid handling the quick action twice we need to check in the willFinish… or didFinish… methods to see if we have a quick action, handle it and then return false. Returning false tells the system not to call performActionForShortcutItem.

To detect if the system launched the app with a quick action we need to look in the launch options dictionary to retrieve the UIApplicationShortcutItem. We can then pass that to our handleShortcut function to handle as before and return false to prevent the system from calling performActionForShortcutItem:

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject : AnyObject]?) -> Bool {
    
  if let shortcutItem =
       launchOptions?[UIApplicationLaunchOptionsShortcutItemKey]
       as? UIApplicationShortcutItem {
        
    handleShortcut(shortcutItem)
    return false
  }
  return true
}

All Done

For static quick actions that is all you need to do. Dynamic actions are not much more complicated but I will maybe cover those another time. The updated sample code is in my Code Examples GitHub repository.

Further Reading