Unable to launch another app from my app's shortcut menu

83 Views Asked by At

I am trying to create an iOS application, using the newest version of Swift, where a user can force touch on our app icon on their home screen. This then displays the options to open the Apple Calendar, Notes, and Maps apps. However, when we tap these, only our app opens and not the respective Apple app after.

Here is my LauncherApp.swift file:

import SwiftUI
import UIKit

class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?

    override init() {
        super.init()
        print("AppDelegate initialized")
    }

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        print("Application did finish launching with options.")

        self.window = UIWindow()
        self.window?.rootViewController = UIHostingController(rootView: ContentView())
        self.window?.makeKeyAndVisible()

        if let shortcutItem = launchOptions?[.shortcutItem] as? UIApplicationShortcutItem {
            print("Launching from Quick Action: \(shortcutItem.type)")
            handleShortcutItem(shortcutItem)
            return false
        }

        return true
    }

    func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
        print("Received a Quick Action while running: \(shortcutItem.type)")
        handleShortcutItem(shortcutItem)
        completionHandler(true)
    }


    private func handleShortcutItem(_ shortcutItem: UIApplicationShortcutItem) {
        print("Handling Shortcut Item: \(shortcutItem.type)")

        switch shortcutItem.type {
        case "com.launcher.openMaps":
            print("Opening Maps...")
            openMaps()
        case "com.launcher.openCalendar":
            print("Opening Calendar...")
            openCalendar()
        case "com.launcher.openNotes":
            print("Opening Notes...")
            openNotes()
        default:
            print("Unhandled Shortcut Item: \(shortcutItem.type)")
        }
    }

    private func openMaps() {
        openApp(withURLScheme: "maps://")
    }

    private func openCalendar() {
        openApp(withURLScheme: "calshow://")
    }

    private func openNotes() {
        openApp(withURLScheme: "mobilenotes://")
    }

    private func openApp(withURLScheme urlScheme: String) {
        print("Attempting to open app with URL Scheme: \(urlScheme)")
        guard let url = URL(string: urlScheme) else {
            print("Invalid URL Scheme: \(urlScheme)")
            return
        }
        
        if UIApplication.shared.canOpenURL(url) {
            print("Opening app with URL Scheme: \(urlScheme)")
            UIApplication.shared.open(url, options: [:], completionHandler: nil)
        } else {
            print("Cannot open URL Scheme: \(urlScheme)")
        }
    }
}

@main
struct LauncherApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

Here is my Info.plist file:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>LSApplicationQueriesSchemes</key>
    <array>
        <string>maps</string>
        <string>mobilenotes</string>
        <string>calshow</string>
    </array>
    <key>UIApplicationShortcutItems</key>
    <array>
        <dict>
                    <key>UIApplicationShortcutItemIconType</key>
                    <string>UIApplicationShortcutIconTypeLocation</string>
                    <key>UIApplicationShortcutItemTitle</key>
                    <string>Open Maps</string>
                    <key>UIApplicationShortcutItemType</key>
                    <string>com.launcher.openMaps</string>
                </dict>
                <dict>
                    <key>UIApplicationShortcutItemIconType</key>
                    <string>UIApplicationShortcutIconTypeDate</string>
                    <key>UIApplicationShortcutItemTitle</key>
                    <string>Open Calendar</string>
                    <key>UIApplicationShortcutItemType</key>
                    <string>com.launcher.openCalendar</string>
                </dict>
                <dict>
                    <key>UIApplicationShortcutItemIconType</key>
                    <string>UIApplicationShortcutIconTypeCompose</string>
                    <key>UIApplicationShortcutItemTitle</key>
                    <string>Open Notes</string>
                    <key>UIApplicationShortcutItemType</key>
                    <string>com.launcher.openNotes</string>
                </dict>
            </array>
</dict>
</plist>

Here is ContentView.swift, which opens Maps perfectly fine:

import SwiftUI

struct ContentView: View {
    var body: some View {
        Button("Test Open Maps") {
            openApp(withURLScheme: "maps://")
        }
    }

    func openApp(withURLScheme urlScheme: String) {
        guard let url = URL(string: urlScheme) else { return }
        if UIApplication.shared.canOpenURL(url) {
            UIApplication.shared.open(url, options: [:], completionHandler: nil)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

I am new to iOS development so sorry for just dumping the code. I have tried testing in a simulator and on my iPhone, but it doesn't work on either.

1

There are 1 best solutions below

0
On

When running under iOS 13 and later, the App Delegate is not used to handle shortcuts. You now need to handle shortcut items in the Scene Delegate.

Add a file named SceneDelegate.swift to your project with the following contents:

import UIKit

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?

    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).
        guard let _ = (scene as? UIWindowScene) else { return }

        // This will be true if a shortcut item is selected while the app is not running
        if let shortcutItem = connectionOptions.shortcutItem {
            _ = handleShortcutItem(shortcutItem)
        }
    }

    // Called if a shortcut item is selected while the app is already running
    func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
        let res = handleShortcutItem(shortcutItem)

        completionHandler(res)
    }

    private func handleShortcutItem(_ shortcutItem: UIApplicationShortcutItem) -> Bool {
        print("Handling Shortcut Item: \(shortcutItem.type)")

        switch shortcutItem.type {
            case "com.launcher.openMaps":
                print("Opening Maps...")
                openMaps()
                return true
            case "com.launcher.openCalendar":
                print("Opening Calendar...")
                openCalendar()
                return true
            case "com.launcher.openNotes":
                print("Opening Notes...")
                openNotes()
                return true
            default:
                print("Unhandled Shortcut Item: \(shortcutItem.type)")
        }

        return false
    }

    private func openMaps() {
        openApp(withURLScheme: "maps://")
    }

    private func openCalendar() {
        openApp(withURLScheme: "calshow://")
    }

    private func openNotes() {
        openApp(withURLScheme: "mobilenotes://")
    }

    private func openApp(withURLScheme urlScheme: String) {
        print("Attempting to open app with URL Scheme: \(urlScheme)")
        guard let url = URL(string: urlScheme) else {
            print("Invalid URL Scheme: \(urlScheme)")
            return
        }

        if UIApplication.shared.canOpenURL(url) {
            print("Opening app with URL Scheme: \(urlScheme)")
            UIApplication.shared.open(url, options: [:], completionHandler: nil)
        } else {
            print("Cannot open URL Scheme: \(urlScheme)")
        }
    }
}

In your AppDelegate.swift, remove the private functions for handling the shortcuts (all of that code is now in SceneDelegate). Remove the performActionFor app delegate method since it isn't used as of iOS 13. Remove the if let... block from the didFinishLaunchingWithOptions app delegate method since that is also handled in the scene delegate.

For a UIKit app, that is all that is needed.

For a SwiftUI app, you need to add the following to your AppDelegate:

// Setup the scene delegate
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
    let configuration = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role)
    if connectingSceneSession.role == .windowApplication {
        configuration.delegateClass = SceneDelegate.self
    }

    return configuration
}

That lets the SwiftUI app know about your SceneDelegate class.

Those changes, combined with your existing setup in Info.plist for the shortcut items and the URL scheme queries, will allow your app to respond to the shortcut items as expected.