This tutorial explains the relationship between UIWindowScene, UIWindow, and UIScene, and shows how to create a secondary debug window using SwiftUI inside a UIKit-hosted UIWindow. This is especially useful for developers working on internal tools or debugging overlays for iOS, iPadOS, or visionOS apps.
Apple's scene-based lifecycle (introduced in iOS 13) allows multiple UI scenes per app. Each scene is an instance of UIScene, and for UIKit-based apps, it takes the form of a UIWindowScene. Each UIWindowScene can own multiple UIWindow instances.
A UIWindow is the visual container for your app's interface. Typically, an app has one UIWindow per scene, but you can create additional windows for overlays or floating utilities.
UIApplication
└── UIScene (abstract)
└── UIWindowScene (concrete, UIKit-specific)
└── UIWindow (visual container)
└── View Controller or SwiftUI view
UIScene: Abstract base class.UIWindowScene: Concrete class for UIKit scenes.UIWindow: Hosts view hierarchies, requires aUIWindowScene.
In this tutorial, you'll learn how to attach a SwiftUI-based debug overlay window to the existing scene. This is useful for logging, inspecting state, or interacting with tools during development.
import SwiftUI
struct DebugOverlayView: View {
var body: some View {
VStack {
Text("Debug Window")
.font(.headline)
.foregroundColor(.white)
Spacer()
Button("Close Debug Info") {
// Hook into dismissal or action
}
.padding()
}
.frame(width: 300, height: 200)
.background(.black.opacity(0.8))
.cornerRadius(12)
.padding()
}
}import UIKit
import SwiftUI
final class DebugWindowController {
private var window: UIWindow?
func show(in scene: UIWindowScene) {
guard window == nil else { return }
let window = UIWindow(windowScene: scene)
window.frame = CGRect(x: 100, y: 100, width: 320, height: 240)
window.windowLevel = .alert + 1
window.rootViewController = UIHostingController(rootView: DebugOverlayView())
window.isHidden = false
self.window = window
}
func hide() {
window?.isHidden = true
window = nil
}
}class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
let debugWindow = DebugWindowController()
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = scene as? UIWindowScene else { return }
// Main window
let mainWindow = UIWindow(windowScene: windowScene)
mainWindow.rootViewController = UIHostingController(rootView: ContentView())
self.window = mainWindow
mainWindow.makeKeyAndVisible()
// Debug window
debugWindow.show(in: windowScene)
}
}- Use
windowLevel = .alert + 1to layer on top of other UI. - Optional: add drag gestures to move the debug overlay.
- Restrict to debug builds using
#if DEBUG. - You can later expand this pattern to use a separate
UISceneif you want full multitasking support.
By understanding the hierarchy of UIScene, UIWindowScene, and UIWindow, and combining it with SwiftUI via UIHostingController, you can create powerful development tools and overlays directly within your app. This pattern is especially useful for diagnostics, development panels, or inspector-style UI components.