- Published on
- 8 min read Intermediate
> Fixing UIKit Apps That Won't Launch on iOS 27: Adopting the Scene Life Cycle
If you maintain an older UIKit app, there's a good chance it still starts life the way UIKit apps did before iOS 13: an app delegate creates a UIWindow in application(_:didFinishLaunchingWithOptions:) and that's the whole story. That setup stops working with Xcode 27. The iOS 27 release notes put it bluntly: apps built with the latest SDK must adopt the scene-based life cycle or they fail to launch. The same rule applies to iPadOS 27, Mac Catalyst 27, tvOS 27, and visionOS 27.
Apple has been warning about this for a while. Starting in iOS 18.4, UIKit logged this message for apps that hadn't migrated:
This process does not adopt UIScene lifecycle.
This will become an assert in a future version.
In iOS 26 it changed to:
UIScene lifecycle will soon be required.
Failure to adopt will result in an assert in the future.
If you've seen either of those in your console and scrolled past, iOS 27 is the future they were talking about. The good news is that the migration is mostly mechanical, and you can do it without supporting multiple windows.
Is Your App Affected?
Apple's migration guide, Transitioning to the UIKit scene-based life cycle, gives two conditions. You need to migrate if the UIApplicationSceneManifest key is missing from your Info.plist (or has no configurations in it), or if your app delegate doesn't implement application(_:configurationForConnecting:options:).
Apps built on SwiftUI's App protocol already run on scenes, so this mostly bites UIKit projects that predate the scene-based templates Xcode 11 introduced, or that had their scene delegate deleted at some point. A quick way to check is to search your target's Info.plist for UIApplicationSceneManifest and your code for UIWindowSceneDelegate. If neither turns up, keep reading.
The requirement is tied to the SDK you build with, which is why it shows up the moment you switch to Xcode 27. Plan on doing the migration as part of that update rather than after it.
Tell UIKit About Your Scene
The simplest route is an Info.plist entry. Apple's steps are to select your app target, check Scene manifest under Deployment Info on the General tab, and make sure the Info.plist ends up with a UIApplicationSceneManifest entry that names your scene delegate. For an app that builds its interface in code, the finished entry looks like this:
<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>
If your window's root view controller comes from a storyboard, add a UISceneStoryboardFile entry with the storyboard's name inside that same dictionary and UIKit will set up the window for you. Leave UIApplicationSupportsMultipleScenes set to false unless you actually want multiple windows. Supporting several scenes at once is optional, and it usually means reworking state that assumes there's only one copy of your UI.
The alternative is to supply the configuration in code. This is handy when you'd rather keep the Info.plist minimal, or when different scenes need different setups:
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// App-wide setup only: analytics, appearance, dependency containers.
return true
}
func application(
_ application: UIApplication,
configurationForConnecting connectingSceneSession: UISceneSession,
options: UIScene.ConnectionOptions
) -> UISceneConfiguration {
let configuration = UISceneConfiguration(
name: "Default Configuration",
sessionRole: connectingSceneSession.role
)
configuration.delegateClass = SceneDelegate.self
return configuration
}
}
Notice what's no longer in didFinishLaunchingWithOptions. The app delegate still handles process-level work, but it stops owning the window.
Move Window Setup Into a Scene Delegate
Everything that used to build your UI moves into scene(_:willConnectTo:options:). The scene delegate creates the window, attaches it to the UIWindowScene it was handed, and keeps a strong reference to it:
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 }
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UINavigationController(
rootViewController: LibraryViewController()
)
window.makeKeyAndVisible()
self.window = window
// Launch-time URLs and quick actions now arrive here, not in launchOptions.
if let url = connectionOptions.urlContexts.first?.url {
DeepLinkRouter.handle(url, in: window)
}
if let shortcut = connectionOptions.shortcutItem {
_ = DeepLinkRouter.handle(shortcut, in: window)
}
}
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
guard let url = URLContexts.first?.url else { return }
DeepLinkRouter.handle(url, in: window)
}
func windowScene(
_ windowScene: UIWindowScene,
performActionFor shortcutItem: UIApplicationShortcutItem,
completionHandler: @escaping (Bool) -> Void
) {
completionHandler(DeepLinkRouter.handle(shortcutItem, in: window))
}
func sceneDidEnterBackground(_ scene: UIScene) {
// Save this scene's state here instead of in applicationDidEnterBackground(_:).
}
}
DeepLinkRouter and LibraryViewController stand in for whatever your app already has. The part people forget is the launch payload. With scenes, a URL or Home Screen quick action that launches your app shows up in UIScene.ConnectionOptions through urlContexts, shortcutItem, and userActivities. The matching launchOptions keys in the app delegate are deprecated as of iOS 26, with messages pointing you to those connection option properties. If your deep links quietly stop working after the migration, that's usually why.
Map Your Life Cycle Callbacks
The four familiar app delegate state callbacks each have a scene counterpart, and several other entry points moved as well. The iOS 26 SDK deprecates the app delegate versions with messages that name the scene API to use instead:
| App delegate method | Scene-based replacement |
|---|---|
applicationDidBecomeActive(_:) | sceneDidBecomeActive(_:) |
applicationWillResignActive(_:) | sceneWillResignActive(_:) |
applicationDidEnterBackground(_:) | sceneDidEnterBackground(_:) |
applicationWillEnterForeground(_:) | sceneWillEnterForeground(_:) |
application(_:open:options:) | scene(_:openURLContexts:) |
application(_:continue:restorationHandler:) | scene(_:continue:) |
application(_:performActionFor:completionHandler:) | windowScene(_:performActionFor:completionHandler:) |
Apple's guide is explicit that once you adopt scenes, UIKit stops calling the four state methods on your app delegate even if you leave them implemented. That catches people who move the window code but leave, say, a "save everything" routine in applicationDidEnterBackground(_:). It will simply never run.
Scene callbacks also describe one scene rather than the whole app. With a single scene that distinction barely matters, but if something genuinely needs app-wide state, such as "is any part of the app in the foreground", Apple recommends observing the UIApplication notifications like willEnterForegroundNotification instead of relying on one scene's delegate.
Replace Global Window Lookups
Code that reaches for UIApplication.shared.keyWindow or UIApplication.shared.windows is worth cleaning up at the same time. Both have been deprecated for a while: keyWindow because it returns a key window across all connected scenes, and windows in favor of the windows array on a relevant window scene.
The best replacement is almost always context. A view controller can use view.window, and from there view.window?.windowScene. For the occasional utility that really has no view to start from, like presenting an alert from a networking layer, you can ask the connected scenes directly. This version uses the window scene's own keyWindow property, which requires iOS 15:
extension UIApplication {
var foregroundKeyWindow: UIWindow? {
connectedScenes
.compactMap { $0 as? UIWindowScene }
.first { $0.activationState == .foregroundActive }?
.keyWindow
}
}
Treat that helper as a last resort. If you later turn on multiple scenes, "the foreground scene" can be more than one thing, and code that starts from a view always knows which window it belongs to.
Test the Migration
Build with Xcode 27, delete the app from the simulator so no stale state hides anything, and launch it. Then walk through the entry points that changed: open a deep link while the app is closed and while it's running, trigger a Home Screen quick action, and background the app to confirm your save logic runs from the scene delegate. Apple also suggests testing on iPad in Full Screen Apps, Windowed Apps, and Stage Manager, since scene-based apps are the ones that participate fully in iPad multitasking.
While you're in the Info.plist, check that it also declares a launch screen. Apps built with the iOS 27 SDK need one of those too, and App Store Connect rejects uploads that don't have it. That one is covered in Fixing ITMS-90870.
For most apps the migration comes down to an Info.plist entry, a new scene delegate class, and moving a handful of methods out of the app delegate. Scenes are also what iPad windowing and the other multitasking features are built on, so the work pays off beyond getting the app to launch again.
Sample Project
Want to see this code in action? Check out the complete sample project on GitHub:
The repository includes a working Xcode project with all the examples from this article, plus unit tests you can run to verify the behavior.
// Continue_Learning
Reading the iPhone Duo Hinge Angle with onHingeChange in SwiftUI
iOS 27.1 lets you read iPhone Duo's hinge angle and fold status with onHingeChange in SwiftUI and UIHingeInteraction in UIKit. Here's how the APIs work, why they're for interactive effects rather than layout, and a fold-to-scrub example.
How to Get Your App Ready for iPhone Duo
iPhone Duo ships on October 23 with a folding inner display, a compact outer display, and toolbars that move to the side. Here's what to check in your app first, and where the new iOS 27.1 APIs fit in.
Leading-Aligned Navigation Titles in UIKit with titleAlignment
iOS 27.2 adds a titleAlignment property to UINavigationItem, so you can pin an inline navigation title to the leading edge or keep it centered. Here's how it works, plus the trait that tells custom title views which alignment the bar picked.
// Stay Updated
Get notified when I publish new tutorials on Swift, SwiftUI, and iOS development. No spam, unsubscribe anytime.