Categories
Manuals

How to add Quick actions

This tutorial contains information on how Quick Actions work in iOS, how to set them up and how to handle actions. Moreover, we will create a simple iOS application with two static quick actions and one dynamic.

Lets begin with static ones. All of them should be defined in Info.plist file in a specific key — UIApplicationShortcutItems. Each child item should be a dictionary and must have at least these required keys:

  1. UIApplicationShortcutItemType — a string which is sent to your application as a part of UIApplicationShortcutItem. It can be used in code to handle actions for different shortcut types;
  2. UIApplicationShortcutItemTitle — a title of your action which will be displayed on the quick action menu. If the title does not fit on one line and you did not specify a subtitle, then it will go on two lines. Can be localised;

And any of these optional keys:

  1. UIApplicationShortcutItemSubtitle — an optional string which defines a subtitle of your action. It will be displayed under your title on the quick actions menu. Can be localised;
  2. UIApplicationShortcutItemIconType — an optional string which defines built-in icon type. A list of available types can be found here;
  3. UIApplicationShortcutItemIconFile — an optional string specifying an image from Assets Catalog or from the Bundle. More information on image sizes and design guidelines can be found here. If this key is specified, then the system ignores UIApplicationShortcutItemIconType;
  4. UIApplicationShortcutItemUserInfo — an optional dictionary of additional information which you may want to parse.

Cannot wait to show you how it works! 

We need to create a new Single View Application (File -> New -> Project).

Next, open Info.Plist file and copy & paste this:

<key>UIApplicationShortcutItems</key>  
<array>  
  <dict>
    <key>UIApplicationShortcutItemIconType</key>
    <string>UIApplicationShortcutIconTypeSearch</string>   
    <key>UIApplicationShortcutItemTitle</key>
    <string>SHORTCUT_TITLE_SEARCH</string
    <key>UIApplicationShortcutItemType</key
    <string>$(PRODUCT_BUNDLE_IDENTIFIER).Search</string>
  </dict>
  <dict>
    <key>UIApplicationShortcutItemTitle</key>
    <string>SHORTCUT_TITLE_FAVORITES</string
    <key>UIApplicationShortcutItemType</key
    <string>$(PRODUCT_BUNDLE_IDENTIFIER).Favorites</string>
  </dict>
</array>  

If you do not have an iPhone 6S or 6S plus, then use this tweak to test on simulator, because currently it does not have any shortcut for 3D touch.

Build your project and try 3D touch on your app icon. It should show you a menu of two items. Really nice and easy, isn’t it?

You have probably noticed two things:

  1. Titles are weird;
  2. When you press on any of these actions it just opens the application without any other action.

Lets fix them!


Titles are weird because we specified them like this in Info.plist. And the only reason for that is to localise them. We need to create InfoPlist.strings file (if you call it differently, Info.plist localisation will not work). Go to File -> New -> File (or Command + N), select iOS -> Resource on the left and choose Strings File.

The next step is to mark this file as localisable. Select file and press “Localize…” in File Inspector (if file inspector is hidden, you can always show it by pressing View -> Utilities -> Show File Inspector). A popup should appear. Select “Base” and press “Localize”. Now this file is localised for Base Localization. We might want to support English localisation, so we have to put a check mark next to the English option in localisation in File Inspector, here:

Now our file should look like this:

Copy and paste this to both files:

"SHORTCUT_TITLE_SEARCH" = "Search";
"SHORTCUT_TITLE_FAVORITES" = "Favorites";

Now build project again. Woohoo, the first issue is fixed!


Our next challenge is to somehow handle these actions. We should always keep in mind that there are two different situations:

  1. The application is closed and the user opens the application via quick action shortcut. In this case application:didFinishLaunchingWithOptions: will be called. launchingOptions will contain UIApplicationShortcutItem stored for key UIApplicationLaunchOptionsShortcutItemKey;
  2. The application was opened before and the user wants to continue usage with a shortcut. In this case the application will call this AppDelegate function: application:performActionForShortcutItem:completionHandler:

I would like to show you how exactly it is used in practise. 

First, let’s create a private function which will handle shortcut items:

private func handleShortcutItem(shortcutItem: UIApplicationShortcutItem) {  
  if let rootViewController = window?.rootViewController {
    rootViewController.dismissViewControllerAnimated(false, completion: nil)
    let alertController = UIAlertController(title: "", message: shortcutItem.localizedTitle, preferredStyle: .Alert)
    alertController.addAction(UIAlertAction(title: "OK", style: .Default, handler: nil))
    rootViewController.presentViewController(alertController, animated: true, completion: nil)
  }
}

Second, let’s add logic to get the shortcut item when the app is opened from quick action. Paste this code inside application:didFinishLaunchingWithOptions:
Please do not forget to check iOS version – if your application supports iOS 8 and lower it will crash.

if let shortcutItem = launchOptions?[UIApplicationLaunchOptionsShortcutItemKey] as? UIApplicationShortcutItem {  
  handleShortcutItem(shortcutItem)
}

And the last step is to declare application:performActionForShortcutItem:completionHandler: which handles quick action when the application is not quit. Put this code in AppDelegate:

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

Run the application and try quick actions again. You must have noticed that when you use quick action, alert controller is displayed with the action title.

In a real application you would want to have a different action for a different shortcut item. As it was mentioned above, UIApplicationShortcutItemType is used to identify action type. Hm.. lets do that!

Paste this code above AppDelegate class declaration:

enum DGShortcutItemType: String {  
    case Search
    case Favorites

    init?(shortcutItem: UIApplicationShortcutItem) {
        guard let last = shortcutItem.type.componentsSeparatedByString(".").last else { return nil }
        self.init(rawValue: last)
    }

    var type: String {
        return NSBundle.mainBundle().bundleIdentifier! + ".\(self.rawValue)"
    }
}

What we do here is easy. Each shortcut item contains a type. In our example it was:

  • $(PRODUCT_BUNDLE_IDENTIFIER).Favorites
  • $(PRODUCT_BUNDLE_IDENTIFIER).Search

We created an enumeration which parses these values and gives us back a clean value – .Search or .Favorites. Having an enum code looks cleaner.

Now replace our previous handleShortcutItem declaration with this:

private func handleShortcutItem(shortcutItem: UIApplicationShortcutItem) {  
        if let rootViewController = window?.rootViewController, let shortcutItemType = DGShortcutItemType(shortcutItem: shortcutItem) {
            rootViewController.dismissViewControllerAnimated(false, completion: nil)
            let alertController = UIAlertController(title: "", message: "", preferredStyle: .Alert)

            switch shortcutItemType {
            case .Search:
                alertController.message = "It's time to search"
                break
            case .Favorites:
                alertController.message = "Show me my favorites"
                break
            }

            alertController.addAction(UIAlertAction(title: "OK", style: .Default, handler: nil))
            rootViewController.presentViewController(alertController, animated: true, completion: nil)
        }
    }

What we’ve done:

  1. We have added let shortcutItemType = DGShortcutItemType(shortcutItem: shortcutItem) to if statement to make sure that the value is not nil. It can be nil if we add a new shortcut but do not add a new case to enum;
  2. Added switch statement, so we can have a different action per shortcut item type.

Run project and test it. Now it should show completely different alert messages for each shortcut.

Hm.. one thing is definitely wrong and I do not like that:

The Favorites shortcut does not have an image and it doesn’t look as nice as the Search shortcut. We should fix that.

Download this file, extract contents and put in Images.xcassets.

Open Info.plist as Source Code and add these two lines to the end of the Favorites item declaration:

<key>UIApplicationShortcutItemIconFile</key>  
<string>ShortcutIconFavorites</string>  

All together our UIApplicationShortcutItems should look like this:

    <key>UIApplicationShortcutItems</key>
    <array>
        <dict>
            <key>UIApplicationShortcutItemIconType</key>
            <string>UIApplicationShortcutIconTypeSearch</string>
            <key>UIApplicationShortcutItemTitle</key>
            <string>SHORTCUT_TITLE_SEARCH</string>
            <key>UIApplicationShortcutItemType</key>
            <string>$(PRODUCT_BUNDLE_IDENTIFIER).Search</string>
        </dict>
        <dict>
            <key>UIApplicationShortcutItemTitle</key>
            <string>SHORTCUT_TITLE_FAVORITES</string>
            <key>UIApplicationShortcutItemType</key>
            <string>$(PRODUCT_BUNDLE_IDENTIFIER).Favorites</string>
            <key>UIApplicationShortcutItemIconFile</key>
            <string>ShortcutIconFavorites</string>
        </dict>
    </array>

Run you project. Now when you do 3D touch on your app icon you should see this:

Now it looks much better and we have learned how we can use custom assets for our quick action shortcuts.


Now lets try to do dynamic shortcut. Starting iOS 9 UIApplication class has public variable public var shortcutItems: [UIApplicationShortcutItem]? with description:

Register shortcuts to display on the home screen, or retrieve currently registered shortcuts.

That is exactly what we are going to use. We will create an interface which allows you to enter data and update/remove the current shortcut.

Open ViewController.swift and replace everything with this:

import UIKit

extension UIApplicationShortcutIconType {  
    var toString: String {
        switch self {
        case .Compose: return "Compose"
        case .Play: return "Play"
        case .Pause: return "Pause"
        case .Add: return "Add"
        case .Location: return "Location"
        case .Search: return "Search"
        case .Share: return "Share"
        }
    }

    init?(string: String) {
        switch string {
        case "Compose": self.init(rawValue: UIApplicationShortcutIconType.Compose.rawValue)
        case "Play": self.init(rawValue: UIApplicationShortcutIconType.Play.rawValue)
        case "Pause": self.init(rawValue: UIApplicationShortcutIconType.Pause.rawValue)
        case "Add": self.init(rawValue: UIApplicationShortcutIconType.Add.rawValue)
        case "Location": self.init(rawValue: UIApplicationShortcutIconType.Location.rawValue)
        case "Search": self.init(rawValue: UIApplicationShortcutIconType.Search.rawValue)
        case "Share": self.init(rawValue: UIApplicationShortcutIconType.Share.rawValue)
        default: return nil
        }
    }

    static var allTypesToStrings: [String] {
        return [UIApplicationShortcutIconType.Compose.toString, UIApplicationShortcutIconType.Play.toString, UIApplicationShortcutIconType.Pause.toString, UIApplicationShortcutIconType.Add.toString, UIApplicationShortcutIconType.Location.toString, UIApplicationShortcutIconType.Search.toString, UIApplicationShortcutIconType.Share.toString]
    }
}

class ViewController: UIViewController {

    // MARK: -
    // MARK: Vars

    private let titleTextField = UITextField()
    private let subtitleTextField = UITextField()
    private var iconTypeSegmentedControl = UISegmentedControl(items: UIApplicationShortcutIconType.allTypesToStrings)
    private let updateButton = UIButton(type: .System)

    // MARK: -

    override func loadView() {
        super.loadView()
        titleTextField.placeholder = "Title"
        titleTextField.delegate = self
        view.addSubview(titleTextField)

        subtitleTextField.placeholder = "Subtitle"
        subtitleTextField.delegate = self
        view.addSubview(subtitleTextField);

        updateButton.setTitle("Update shortcut", forState: .Normal)
        updateButton.addTarget(self, action: Selector("updateDynamicAction"), forControlEvents: .TouchUpInside)
        view.addSubview(updateButton)

        iconTypeSegmentedControl.selectedSegmentIndex = 0
        view.addSubview(iconTypeSegmentedControl)

        view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: "viewTapped"))
    }

    // MARK: -
    // MARK: Methods

    func viewTapped() {
        view.endEditing(true)
    }

    func updateDynamicAction() {
        guard let title = titleTextField.text else {
            UIApplication.sharedApplication().shortcutItems = nil
            return
        }

        let type = NSBundle.mainBundle().bundleIdentifier! + ".Dynamic"
        let shortcutIconType = UIApplicationShortcutIconType(string: iconTypeSegmentedControl.titleForSegmentAtIndex(iconTypeSegmentedControl.selectedSegmentIndex)!)!
        let icon = UIApplicationShortcutIcon(type: shortcutIconType)

        let dynamicShortcut = UIApplicationShortcutItem(type: type, localizedTitle: title, localizedSubtitle: subtitleTextField.text, icon: icon, userInfo: nil)
        UIApplication.sharedApplication().shortcutItems = [dynamicShortcut]
    }

    // MARK: -
    // MARK: Layout

    override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()

        let width = view.bounds.width

        let horizontalMargin: CGFloat = 20.0
        let elementHeight: CGFloat = 40.0
        let verticalSpacing: CGFloat = 20.0

        titleTextField.frame = CGRect(x: horizontalMargin, y: 50.0, width: width - 2 * horizontalMargin, height: elementHeight)
        subtitleTextField.frame = CGRectOffset(titleTextField.frame, 0.0, elementHeight + verticalSpacing)
        iconTypeSegmentedControl.frame = CGRectOffset(subtitleTextField.frame, 0.0, elementHeight + verticalSpacing)
        updateButton.frame = CGRectOffset(iconTypeSegmentedControl.frame, 0.0, elementHeight + verticalSpacing)
    }

}

// MARK: -
// MARK: UITextField Delegate

extension ViewController: UITextFieldDelegate {

    func textFieldShouldReturn(textField: UITextField) -> Bool {
        if textField == titleTextField {
            subtitleTextField.becomeFirstResponder()
        } else if textField == subtitleTextField {
            subtitleTextField.resignFirstResponder()
        }

        return true
    }

}

It looks like lots of code, but it is very basic and does not require explanation.

Run project, you must see a simple interface.

Add some data and press “Update shortcut”. Try 3D touch on your app without closing it, just minimise. You should see something like this:

Have a play with this, try to change values, press “update shortcut” and it will change. If you leave the title label empty and ask for update, then the shortcut will be removed. We can always add, remove, change shortcuts.

There is one thing left which I am not happy with – the app does not do anything when you open the app using Dynamic shortcut. Lets add some alerts, so we know it works.

Here is what we have to do:

  • Add case Dynamic to DGShortcutItemType enumeration to support this type;
  • Add this code at the end of switch statement in handleShortcutItem method:
case .Dynamic:  
    alertController.message = "Dynamic shortcut works!"
    break
  • In updateDynamicAction method change NSBundle.mainBundle().bundleIdentifier! + ".Dynamic" to DGShortcutItemType.Dynamic.type

Run project and test 3D touch on dynamic shortcut. When you open the app, it should show you a specific message as per other shortcuts. Great, isn’t it?

I think we’ve covered main Quick Actions logic. The source code of this tutorial can be found here.

I hope you enjoyed it, please leave your comments and suggestions below. See you next time! :)

Leave a Reply

Your email address will not be published. Required fields are marked *