Skip to content

Instantly share code, notes, and snippets.

@mbernson
Last active April 17, 2025 14:52
Show Gist options
  • Save mbernson/52daeb81a5a1ded80c5060f4845fd1d0 to your computer and use it in GitHub Desktop.
Save mbernson/52daeb81a5a1ded80c5060f4845fd1d0 to your computer and use it in GitHub Desktop.
How to adopt the UIScene lifecycle in an iOS app

How to adopt the UIScene lifecycle in an iOS app

Old UIKit apps only have an AppDelegate, but according to this log message I got in Xcode, the UIScene lifecycle with a SceneDelegate will be required at some point in the future:

CLIENT OF UIKIT REQUIRES UPDATE: This process does not adopt UIScene lifecycle. This will become an assert in a future version.

I've had to figure this out for several apps, and started off by creating a new application and looking at its boilerplate code. My apps also had the following special requirements:

  • Use programmatically defined UI (no Storyboards in the app). This requires some special configuration in Info.plist.
  • Implement Universal Links and URL scheme links. This requires implementing the correct hooks in the UIWindowSceneDelegate.

The following is also taken care of:

  • Correctly configuring Firebase and Crashlytics
  • Applying any UIKit appearance customizations
  • Provide hooks for setting up your application data

The code listed below is based on some popular production apps and has proven itself to be correct.

//
// AppDelegate.swift
//
// Copyright © 2024 Q42. All rights reserved.
//
import UIKit
import FirebaseCore
import FirebaseCrashlytics
import Factory
@main
class AppDelegate: NSObject, UIApplicationDelegate {
// MARK: App setup
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
configureFirebase()
configureCrashlytics()
applyAppearance()
performMigrations()
performAppSetup()
return true
}
/// Sets the global appearance of UIKit components using their appearance proxies.
private func applyAppearance() {
UIPageControl.appearance().currentPageIndicatorTintColor = UIColor.brandPrimaryForeground
UIPageControl.appearance().pageIndicatorTintColor = UIColor.brandTertiaryBackground
let appearance = UINavigationBarAppearance()
appearance.largeTitleTextAttributes = [
.foregroundColor: UIColor.brandPrimaryForeground,
.font: UIFont(name: "My Cool Font Regular", size: 42)!
]
appearance.titleTextAttributes = [
.foregroundColor: UIColor.brandPrimaryForeground,
.font: UIFont(name: "My Cool Font Regular", size: 18)!
]
appearance.backgroundEffect = nil
appearance.backgroundColor = .brandPrimaryBackground
UINavigationBar.appearance().standardAppearance = appearance
}
private func configureFirebase() {
// Configure Firebase SDK
let firebaseConfigFileName = Bundle.main.object(forInfoDictionaryKey: "FIREBASE_CONFIGURATION_FILE") as! String
let firebaseConfigPath = Bundle.main.path(forResource: firebaseConfigFileName, ofType: "plist")!
let firebaseOptions = FirebaseOptions(contentsOfFile: firebaseConfigPath)!
FirebaseApp.configure(options: firebaseOptions)
}
private func configureCrashlytics() {
let isSwiftUIPreview = ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"
// Additional var keeps room for additional critera to be added later
let crashlyticsEnabled = !isSwiftUIPreview
Crashlytics.crashlytics().setCrashlyticsCollectionEnabled(crashlyticsEnabled)
}
/// Performs the initial app setup, if necessary (on the first launch). This includes:
/// - <enter info here>
private func performAppSetup() {
}
/// Performs data migrations from previous versions of the app, if necessary.
private func performMigrations() {
}
// MARK: UISceneSession Lifecycle
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
// Called when a new scene session is being created.
// Use this method to select a configuration to create the new scene with.
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
// Called when the user discards a scene session.
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
}
}
<?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>
<!-- UIScene manifest configuration -->
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneConfigurationName</key>
<string>Default Configuration</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
</dict>
</array>
</dict>
</dict>
</dict>
</plist>
//
// SceneDelegate.swift
//
// Created by Mathijs Bernson on 11/12/2024.
// Copyright © 2024 Q42. All rights reserved.
//
import UIKit
import SwiftUI
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
let viewModel = RootViewModel()
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
let window = UIWindow(windowScene: windowScene)
let rootView = RootView(viewModel: viewModel)
// Using SwiftUI here, it may just as well be a UIKit-based viewcontroller, of course
window.rootViewController = UIHostingController(rootView: rootView)
self.window = window
window.makeKeyAndVisible()
// Check for incoming URLs
handleURLContexts(connectionOptions.urlContexts)
for userActivity in connectionOptions.userActivities {
handleUserActivity(userActivity)
}
}
func handleURLContexts(_ urlContexts: Set<UIOpenURLContext>) {
for context in urlContexts {
viewModel.openURL(url: context.url)
}
}
func scene(_ scene: UIScene, openURLContexts urlContexts: Set<UIOpenURLContext>) {
handleURLContexts(urlContexts)
}
func handleUserActivity(_ userActivity: NSUserActivity) {
if userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let url = userActivity.webpageURL {
viewModel.openURL(url: url)
}
}
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
handleUserActivity(userActivity)
}
func sceneDidDisconnect(_ scene: UIScene) {
// Called as the scene is being released by the system.
// This occurs shortly after the scene enters the background, or when its session is discarded.
// Release any resources associated with this scene that can be re-created the next time the scene connects.
// The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead).
}
func sceneDidBecomeActive(_ scene: UIScene) {
// Called when the scene has moved from an inactive state to an active state.
// Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
}
func sceneWillResignActive(_ scene: UIScene) {
// Called when the scene will move from an active state to an inactive state.
// This may occur due to temporary interruptions (ex. an incoming phone call).
}
func sceneWillEnterForeground(_ scene: UIScene) {
// Called as the scene transitions from the background to the foreground.
// Use this method to undo the changes made on entering the background.
}
func sceneDidEnterBackground(_ scene: UIScene) {
// Called as the scene transitions from the foreground to the background.
// Use this method to save data, release shared resources, and store enough scene-specific state information
// to restore the scene back to its current state.
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment