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 + 1
to 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
UIScene
if 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.