skip to Main Content

In WidgetKit, I am using widgetURL to construct a custom URL schema, so that I can launch the main app via deep link.

I am trying to handle a custom URL scheme (widget://) in SceneDelegate, by launching a view controller from a root view controller.

Here’s my code of launching a view controller from a root view controller.

private func handleIncomingURL(_ url: URL) {
    if let scheme = url.scheme, scheme == "widget" {

        if let rootViewController = self.window?.rootViewController {
            // https://stackoverflow.com/questions/33520899/single-function-to-dismiss-all-open-view-controllers
            // Dismiss all previous launched VC except root view controller.
            rootViewController.dismiss(animated: false, completion: nil)
            
            let greenViewController = GreenViewController.instanceFromMainStoryBoard()
            
            rootViewController.present(greenViewController, animated: true)
        } else {
            print("no rootViewController")
        }
    }
}

There are 2 use cases when handling custom URL scheme.

  1. Previous app is still in app stack. Previous app is restored from the app stack.
  2. The app is not found in app stack. New app is launched.

Previous app is still in app stack

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?


    // Called on existing scenes
    func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
        if let url = URLContexts.first?.url {
            handleIncomingURL(url)
        }
    }
}

The app is not found in app stack

// Called on new scenes
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    guard let _ = (scene as? UIWindowScene) else { return }
    
    if let url = connectionOptions.urlContexts.first?.url {
        handleIncomingURL(url)
    }
}

Thing works well when previous app is still in app stack. rootViewController is not nil, scene(_:openURLContexts:) is called and our greenViewController can launched without issue.

However, when the app is not found in app stack, scene(_:willConnectTo:) is called and rootViewController is nil. Hence, we are not able to launch our greenViewController.

May I know, what is the appropriate action to launch a view controller, in scene(_:willConnectTo:) when root controller is nil?

2

Answers


  1. Inside the func scene(willConnectTo session) method:

        func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
            //
            // other stuff
            //
                    
            if let url = connectionOptions.urlContexts.first?.url {
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) { [weak self] in
                    guard let self = self else { return }
                    self.handleIncomingURL(url)
                     
                    /* ------ for debugging, this should popup after a fresh start: -------- */
                    let rootExists = self.window?.rootViewController != nil
                    
                    let alertController = UIAlertController(title: url.absoluteString, message: "Root exists: (rootExists)", preferredStyle: .alert)
                    let okAction = UIAlertAction(title: "OK", style: .default) { _ in }
                    alertController.addAction(okAction)
                    self.window?.rootViewController?.present(alertController, animated: true)
                }
            }
        }
    

    The 0.7 second delay is IMO a reasonable amount of time the app needs to load a root view controller. Furthermore, since I use this for deeplinks, it is a more appealing experience for the user if the deeplink happens after some delay, and not immediately.

    Login or Signup to reply.
  2. There is a reliable way to do it, but not using the scene(_:willConnectTo) just in case you would consider an alternative approach.

    Here is the ‘modern’ way to handle deep links:

    You place the following code in SwiftUI

    extension ContentView {
        private var deepLinkWidgetSection: some View {
            Section(header: Text("DeepLink Widget")) {
                Text("")
                    .onOpenURL { url in
                        if url.scheme == "widget-DeepLinkWidget", url.host == "widgetFamily" {
                            let widgetFamily = url.lastPathComponent
                            print("Opened from widget of size: (widgetFamily)")
                        }
                    }
            }
        }
    }
    

    This code comes from the GitHub project https://github.com/pawello2222/WidgetExamples

    You can prove this works reliably by first running the app. Then edit your Home Screen to add the Deep Link widget (there are a few in the galley under Example Widgets; keep swiping until you get to it).

    Then you kill off the actual WidgetExamples app using the swipe up from bottom of screen gesture. This removes the app from the "App Stack" (the list of notionally running apps from the end user’s perspective).

    Then you can set a breakpoint in the if url.scheme line.
    Then in Xcode you can Debug > Attach to Process by PID or Name and type in WidgetExamples.

    Now when you tap on the Deep Link widget, it will launch the WidgetExamples app, and hit your breakpoint. The app is properly launched and showing a user interface before your breakpoint is hit.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search