8

My Problem

My app has its own Shortcuts actions created using Intents Extensions. They perform background actions perfectly.

For some actions, I'm trying to make the intent extension open the main (container) app when run in Shortcuts and perform a function.

I'm having trouble with NSUserActivity and I'm not sure if it's the fact it's a SwiftUI project or the way I'm implementing it (or both).

What I've tried

I have registered my NSUserActivity name as an NSUserActivityType in my info.plist ("com.me.project.activityName").

I've added the code below to my AppDelegate.

I initialise a new NSUserActivity inside my intent extension with the same type as the one declared in info.plist.

I've also tried declaring the activity within the app (I don't think I need to do this?)

I'm running: iOS (iPhone XS, Beta 6) macOS 10.15 Catalina (Beta 5) Xcode 11.0 (Beta 5)

My Code Thus Far

I have this in my AppDelegate:

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

          if userActivity.activityType == "com.me.project.activityName" {
              if let url = URL(string: "https://www.google.com") {
                  UIApplication.shared.open(url)
              }
              return true
          }
          return false
      }

This is within my intent extension:

let openApp = intent.launchApp?.boolValue ?? false
        if openApp {

            let UA = NSUserActivity(activityType: "com.me.project.activityName")
            UA.title = "Dummy title"

            completion(UserActivityTestIntentResponse(code: .continueInApp, userActivity: UA))

        } else {
            completion(UserActivityTestIntentResponse.success(result: "You chose not to open the app with a user activity."))
        }

In info.plist

<key>NSUserActivityTypes</key>
    <array>
        <string>com.me.project.activityName</string>
    </array>

I have this declared in a swift file in my project (though I don't think I need it):

let openURLActivityType = "com.me.project.activityName"

let viewPageActivity: NSUserActivity = {
    let userActivity = NSUserActivity(activityType: openURLActivityType)
    userActivity.title = "Dummy Title"
    userActivity.suggestedInvocationPhrase = "Dummy phrase"
    userActivity.isEligibleForSearch = false
    userActivity.isEligibleForPrediction = false
    return userActivity
}()

Unexpected Results

I'm expecting that when the action is run in Shortcuts, my app opens and the website "https://www.google.com".

Currently, after running the action in Shortcuts, my app launches to the home screen and nothing else happens. No breakpoints appear to be hit in my appDelegate.

I can't work out if it's because I'm using NSUserActivity wrong, whether it's because it's SwiftUI or whether it's something that just won't work from inside an intent.

Thank you so much in advance for any help!

mralexhay
  • 1,164
  • 1
  • 10
  • 16
  • 1
    I am having the same issue. For me though I am just using `NSUserActivity` and trying to implement handoff and Spotlight search functionality (`isEligibleForSearch`). For me as well it seems like no breakpoints are hit in my `appDelegate`. I am using macOS Catalina 10.15 beta 7, iOS 13 beta 8 and iPhone X running Xcode 11 beta 7 – l30c0d35 Sep 04 '19 at 17:33
  • 1
    I got it mostly working add this to my scene delegate: `func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { if let intent = userActivity.interaction?.intent as? YourIntent { print(“do something”) } }` However it doesn’t trigger if the app is closed to start with so I’m still trying to get it running the app delegate. – mralexhay Sep 07 '19 at 09:05
  • Thanks, that helped me out a lot. I figured out how you can continue the UserActivity when the app is closed. Check out my answer below – l30c0d35 Sep 07 '19 at 14:40

2 Answers2

12

Like you already commented you can use func scene(_ scene: UIScene, continue userActivity: NSUserActivity) to continue the NSUserActivity when the app is still in the background.

However when the app is closed you can implement let userActvity = connectionOptions.userActivities.first inside the scene(_:willConnectTo:options:) method inside the SceneDelegate to access the activity the user wants to continue.

Just adding this piece of code to the standard implementation of this method would look like this:

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
    // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
    // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).

    // Use a UIHostingController as window root view controller
    if let windowScene = scene as? UIWindowScene {
        let window = UIWindow(windowScene: windowScene)
        window.rootViewController = UIHostingController(rootView: ContentView())
        self.window = window
        window.makeKeyAndVisible()

        //Standard implementation until here
        if let userActvity = connectionOptions.userActivities.first {
            if let intent = userActivity.interaction?.intent as? YourIntent {
                print(“do something”)
            }
        }
    }
}

This works for Siri Intents with Swift 5 Xcode 11.

For Handoff though the .userActivities should not be used as the documentation says, instead the associated delegate methods will be called (scene(_:continue:)):

This property does not contain user activity objects related to Handoff. At connection time, UIKit delivers only the type of a Handoff interaction in the handoffUserActivityType property. Later, it calls additional methods of the delegate to deliver the NSUserActivity object itself.

l30c0d35
  • 777
  • 1
  • 8
  • 32
  • Funny, I figured this out this afternoon too. Thanks for your help! – mralexhay Sep 07 '19 at 17:03
  • Doesn't quite work for me. The last piece should be ```if let _ = connectionOptions.userActivities.first?.interaction?.intent as? YourIntent { //your code }``` – leonboe1 Nov 12 '20 at 18:49
  • what you suggest to do in one if-statement is separated out into two in my answer – l30c0d35 Nov 14 '20 at 19:06
6

If your application is a SwiftUI app, maybe you should consider to add something like:

ContentView
   .onContinueUserActivity("com.company.app.activityIdentifier") { userActivity in 
      handleShortcut(with: userActivity)
   }

where handleShortcut is getting userInfos from the given NSUserActivity and launching things to be done from this opening of your app

This replace any implementations made in the func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool delegate method that is not called anymore in your SwiftUI application unless you make a few more (and not trivial) lines of code to make it called

You can find more details on :

jgodon
  • 241
  • 4
  • 4
  • Thanks for your answer. The original question was before SwiftUI’s App protocol was announced but this is the way I’ve been doing it since. – mralexhay Mar 25 '21 at 17:50