Andrew Graham

Demystifying Siri, Part 3: Restoring User Activity

In Part 2, we created a custom intent in SiriKit to allow us to open our NumberRace app when our Solve Game shortcut is invoked. Next, we’re going to update NumberRace so that our solver is opened and populated with the data we provided.

Restoration

Firstly our application needs to respond to the invocation of an intent. In order to do this we need to implement the application (_ application: UIApplication, continueUserActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) method in our application delegate. In this method we identify which intent was invoked and pass any data to one or more view controllers to process. We’re finally going to do some coding! Open up AppDelegate.swift and add the following:

func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {

    guard userActivity.activityType == "SolveGameIntent" else {
        return false
    }

    guard let window = window,
        let rootViewController = window.rootViewController as? SwitchViewController else {
            return false
    }

    restorationHandler([rootViewController])
    return true
}

My root view controller is the main menu screen and I’ve decided that it should be the lucky recipient of the intent data. Why? Well it’s the only view controller that I’m sure will be instantiated in our app at all times. From there we’ll open my solver’s view controller, predictably named SolverViewController, and pass in our data at the same time.

A segue about segues

An aside: the NumberRace app has a number of view controller scenes - scenes that appear while playing the game, scenes for changing settings, and so on. The scenes are connected with a number of UIStoryboardSegues. I’ve written a helper class called ViewCoordinator that handles the task of opening our solver view controller (SolverViewController) no matter where you left the app. You may be in the middle of a game, or in a settings screen - ViewCoordinator attempts to perform and unwind the segues necessary to open the solver.

The ViewCoordinator code is outside the scope of this blog post series. (By which I mean that I am too embarrassed to share it as it’s not the most elegant thing I’ve written. If I knock it into shape I’ll update this blog post with the code.)

Restoring user activity state

When the line

restorationHandler([viewController1, viewController2, ...])

is executed in a continueUserActivity function, all the restoreUserActivityState(_ activity: NSUserActivity) functions in the view controller list are run.

Therefore, for us, we now need to add such a function to our root view controller.

override func restoreUserActivityState(_ activity: NSUserActivity) {
    if #available(iOS 12.0, *) {
        guard let intent = activity.interaction?.intent as? SolveGameIntent else {
            return
        }

        ViewCoordinator.helper.solverData = (
            number1: intent.number1.rawValue,
            number2: intent.number2.rawValue,
            number3: intent.number3.rawValue,
            number4: intent.number4.rawValue,
            number5: intent.number5.rawValue,
            number6: intent.number6.rawValue,
            target: intent.target
        )
    }

    ViewCoordinator.helper.openSolver()
}

The code here is pretty straightforward. Firstly, we’ll check that our custom intent is of type SolveGameIntent before proceeding. We’ll then add our intent data to ViewCoordinator. Finally, by calling my openSolver() function our ViewCoordinator will handle all the segues that need to be performed to bring our solver into view.

I’ve also created a SolverData data type. I could’ve passed around the SolveGameIntent intent as is, but custom intents are only supported in iOS 12 and above. It seemed more straightforward to convert the data into a tuple instead of adding iOS version checks everywhere in the code.

Incidentally, when you create an intent definition file, Xcode translates this into custom generated code behind the scenes. You can see this generated code by right-clicking on SolveGameIntent appearances in your code and selecting Jump to Definition. Thought it’s worth pointing out here in case you were wondering how Xcode is aware of intent classes.

Trying it out

And that’s all we need! Let’s try it out and create a shortcut…

A screenshot of the Shortcuts app, displaying an example Solve Game shortcut
A Solve Game shortcut

If we run the shortcut by pressing Play, the application continueUserActivity function is activated, our root view controller’s restoreUserActivityState() function is called and then openSolver() opens our solver and populates the values.

The NumberRace Solver screen with the correct fields populated.
The NumberRace Solver screen

So far so good! Our restoration handler is working as expected. But how can we publicise the fact that this functionality is available to a user? The answer lies in suggestions, which we’ll cover next, in Part 4.