Adding Scene Delegate to Existing Project

Mustafa Altiparmak
Appcent
Published in
4 min readMar 26, 2024

I am working on a legacy project that supports iOS 10 and above. Recently, I encountered an interesting task: universal links were not working as expected when the app was completely closed. The links were correctly set up and functioned properly when the app was open (as the continueUserActivity method got triggered). However, when the app was launched by clicking a universal link, the link was not found in the launchOptions in the didFinishLaunchingWithOptions method. After some research, we decided to add SceneDelegate to the project solely to handle universal or deep links.

It’s important to remember that this setup is specific to our app. We need to support iOS versions both before and after iOS 13, as SceneDelegate introduced in iOS 13. Our app is designed with storyboards and doesn’t use a programmatic interface. We’re not using multi-window support because our app only needs one scene. This approach is specific to our situation and may not apply to apps that require multiple scenes.

App Delegate vs Scene Delegate

In iOS app development, the roles of AppDelegate and SceneDelegate are distinct yet complementary. Before iOS 13, AppDelegate was not just the main entry point for your app but also the main point for managing both the application’s lifecycle and its user interface. While AppDelegate continues to be the main entry point and handles overall application lifecycle events, the introduction of SceneDelegate represents a shift in UI management. SceneDelegate specifically handles UI scenes, allowing for more refined control in multitasking environments, especially in applications supporting multiple windows.

Implementation

1- Add SceneDelegate File
In the process of adding SceneDelegate to the project, the first step was creating a new Swift file named ‘SceneDelegate.swift’. In this file, I created a new class named SceneDelegate that inherits from UIResponder, similar to what AppDelegate does. I also made sure this new class implements all the requirements of the UIWindowSceneDelegate protocol.

@available(iOS 13.0, *)
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
}

The @available(iOS 13.0, *) line was added to make sure this class is used only in iOS 13 and later versions. This is important because the app also needs to work on iOS versions older than 13, and this line prevents any compatibility issues.

2- window Property
In iOS app development, the window property holds a reference to the app’s main window. This is crucial for both programmatic and storyboard-based approaches, though its usage differs.

For storyboard-based apps, this setup is automatic. However, when supporting multiple iOS versions with SceneDelegate, it’s necessary to declare var window: UIWindow? in both AppDelegate and SceneDelegate. This ensures the app functions correctly regardless of whether it’s running on iOS 13 and above, where SceneDelegate is used, or on earlier versions, which rely solely on AppDelegate.

// AppDelegate.swift for iOS 12 and below
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
}

// SceneDelegate.swift for iOS 13 and above
@available(iOS 13.0, *)
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
}

// AppDelegate.swift (iOS 12 and below)
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
window = UIWindow()
window?.rootViewController = ViewController() // Your custom view controller
window?.makeKeyAndVisible()
return true
}
}

// SceneDelegate.swift (iOS 13 and above)
@available(iOS 13.0, *)
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
window = UIWindow(windowScene: windowScene)
window?.rootViewController = ViewController() // Your custom view controller
window?.makeKeyAndVisible()
}
}

I wrote an extension to get the window to use its root view controller to navigate within the app without needing to keep a reference to the window or the root view controller throughout the app.

extension UIApplication {
var currentWindow: UIWindow? {
if #available(iOS 13.0, *) {
// For iOS 13 and later, grab the window from the SceneDelegate of the first UIWindowScene
return (connectedScenes.first?.delegate as? SceneDelegate)?.window
} else {
// For iOS versions before 13, use AppDelegate's window
return (delegate as? AppDelegate)?.window
}
}
}

3- Example Usage

let storyboard = UIStoryboard(name: "Main", bundle: nil)
let tabbarController = storyboard.instantiateViewController(withIdentifier: "TabBarVC") as! TabBarVC
tabbarController.selectedIndex = 0
UIApplication.shared.currentWindow?.rootViewController = tabbarController
//Installs the view controller’s view as the content view of the window.
//If the window has an existing view hierarchy, the old views are removed before the new ones are installed.

Comparing AppDelegate and SceneDelegate Methods

In iOS 13 and later, certain responsibilities have shifted from AppDelegate to SceneDelegate. Here’s a quick comparison of equivalent methods in both:

App Initialization:

  • AppDelegate: application(_:didFinishLaunchingWithOptions:)

Scene Initialization:

  • SceneDelegate: scene(_:willConnectTo:options:)

Handling Universal Links (when app is running):

  • AppDelegate: application(_:continue:userActivity:)
  • SceneDelegate: scene(_:continue:userActivity:)

Opening URLs:

  • AppDelegate: application(_:open:options:)
  • SceneDelegate: scene(_:openURLContexts:)

App and Scene Lifecycle:

  • AppDelegate: applicationWillResignActive(_:) and applicationDidEnterBackground(_:)
  • SceneDelegate: sceneWillResignActive(_:) and sceneDidEnterBackground(_:)

Reactivating App or Scene:

  • AppDelegate: applicationWillEnterForeground(_:) and applicationDidBecomeActive(_:)
  • SceneDelegate: sceneWillEnterForeground(_:) and sceneDidBecomeActive(_:)

Notifications
In iOS 13 and later, notifications can still be handled in AppDelegate.

iOS Version and Delegate Method Calls

When using SceneDelegate in your app, keep in mind that iOS 13 and newer versions will use SceneDelegate methods for certain tasks, if implemented. For example, scene-related life cycle events and URL handling will be managed by SceneDelegate instead of AppDelegate. In contrast, for iOS versions below 13 where SceneDelegate isn’t available, the system will continue to use AppDelegate methods. It’s essential to code your app to handle both scenarios, ensuring smooth operation across different iOS versions.

Finally, Universal Link Handling When the App is Closed

With the introduction of SceneDelegate, I can now seamlessly handle universal links even when the app is closed. Before iOS 13, this was limited to AppDelegate and only worked when the app was open.

Now, with SceneDelegate, I can use the scene(_:willConnectTo:options:) method to catch universal links as the app starts. Here’s a basic example:

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
if let userActivity = connectionOptions.userActivities.first {
if userActivity.activityType == NSUserActivityTypeBrowsingWeb {
// Handle the universal link here
}
}
}

--

--