-1

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?

Cheok Yan Cheng
  • 47,586
  • 132
  • 466
  • 875
  • I do not know what "app stack" means, or "previous app" or "new app" for that matter; but I cannot find a situation, using just the code you have shown (and assuming you're using a storybard), where the root view controller is `nil` at launch when `handleIncomingURL` is called with an external URL. There is _always_ a root view controller when the app is launched thru a custom scheme. If you think not, you need to show how that's even possible. – matt Dec 30 '22 at 17:13
  • @matt when you double tap on home button (iPhone SE), there are 2 possibility - either the previous main app instance remains in the list (I call that as app stack. Please correct me if I were wrong), or there is no main app instance. the root controller is nil when scene(willConnectTo) is called. this can be reproduce with a home widget tapping. before tapping, remove the main app from the list by double tap on home button. – Cheok Yan Cheng Dec 30 '22 at 18:07
  • Okay, but there is _no widget in your question_. You simply asked about a normal app that is launched thru a custom scheme. If you test that, you will discover that there is always a root view controller — even after a totally cold start of the device. – matt Dec 30 '22 at 18:09
  • Hem... I will rephrase the question content. To be precise, in WidgetKit, I am using `widgetURL` to construct a custom URL schema, so that I can launch the main app via deep link. – Cheok Yan Cheng Dec 30 '22 at 18:12
  • And then this will be identical to your _other_ question (why did you ask the same question in two ways at the same time?). – matt Dec 30 '22 at 18:13

2 Answers2

0

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.

Faisal Memon
  • 2,711
  • 16
  • 30
-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.

birkoof
  • 173
  • 8
  • Having to delay "N" amount of time sound a bit scary because it doesn't guarantee we will get a non nil view controller. Do you think we can have a more definite way to obtain a non nil view controller? – Cheok Yan Cheng Dec 30 '22 at 16:29