ScreenNameViewer is a debugging tool that overlays the class name of the currently displayed screen.
It allows you to intuitively check which screen is active, and in a SwiftUI environment, it can also display the NavigationStack route.
This allows you to quickly find and navigate to the code for the desired screen, improving both debugging and development efficiency.
- Real-time class name display: Shows
UIViewControllerclass names andNavigationStackroute on screen in real-time - Automatic lifecycle tracking: Automatically tracks all
UIViewControllers using method swizzling at the application level - Debug-only: All internal code wrapped in
#if DEBUG— automatically disabled in RELEASE builds with zero runtime cost - UI customization: Freely configure text size, color, vertical position, etc.
- Memory safe: Prevents memory leaks using weak references and automatic cleanup
- Touch interaction: Tap label to display full class name in toast — non-label areas pass through, never blocking the underlying app
- Both SwiftUI and UIKit: One library covers both frameworks
In Xcode, File → Add Package Dependencies... and enter:
https://github.com/DongLab-DevTools/ScreenNameViewer-For-iOS
Or add directly to Package.swift:
dependencies: [
.package(url: "https://github.com/DongLab-DevTools/ScreenNameViewer-For-iOS", from: "1.0.0")
]Add to your target's dependencies:
.target(
name: "MyApp",
dependencies: ["ScreenNameViewer"]
)- iOS 16.0 or higher
- Swift 5.9 or higher (Xcode 15+)
Call ScreenNameViewer.start() once in your AppDelegate. Every UIViewController is then automatically tracked via method swizzling — no further code changes needed.
import UIKit
import ScreenNameViewer
@main
final class AppDelegate: UIResponder, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
ScreenNameViewer.start()
return true
}
}The left label automatically displays the class name of the currently visible UIViewController.
import SwiftUI
import ScreenNameViewer
@main
struct MyApp: App {
init() {
ScreenNameViewer.start()
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}This alone tracks every screen, but SwiftUI views are hosted by UIHostingController whose class name is filtered out as framework noise. To show meaningful names for SwiftUI screens, add the modifiers below.
Apply once on the root NavigationStack. Push/pop transitions automatically update the right label.
struct ContentView: View {
@State private var path: [Route] = []
var body: some View {
NavigationStack(path: $path) {
// ...destinations
}
.trackScreenName(path: path)
}
}For screens outside the NavigationStack path, declare the name explicitly:
.sheet(isPresented: $showSheet) {
SheetView()
.trackScreenName("StandardSheet")
}
.fullScreenCover(isPresented: $showCover) {
CoverView()
.trackScreenName("FullScreenCover")
}
TabView {
HomeView()
.trackScreenName("Tab.Home")
.tabItem { Label("Home", systemImage: "house") }
}Stack-friendly — when a sheet is on screen, its name takes precedence; on dismissal the previous value is automatically restored.
Customize the overlay appearance via start { config in ... }:
ScreenNameViewer.start { config in
// Left label — UIViewController name
config.viewController.textColor = .white
config.viewController.backgroundColor = UIColor.black.withAlphaComponent(0.7)
config.viewController.textSize = 12
config.viewController.enabled = true
// Right label — NavigationStack route
config.route.textColor = .systemYellow
config.route.backgroundColor = UIColor.black.withAlphaComponent(0.7)
config.route.textSize = 12
// Vertical position (top / bottom). Horizontal placement is fixed (left/right).
config.verticalPosition = .top
}-
viewController / route: Style for each label
textColor: Text colorbackgroundColor: Background colortextSize: Text sizeenabled: Whether the label is visiblepaddingHorizontal/paddingVertical: Internal paddingcornerRadius: Corner radius
-
verticalPosition: Vertical position of the overlay (
.top/.bottom) Horizontal position is fixed: viewController on the left, route on the right
The name shown in the overlay is normalized to always be a symbol from the user's own codebase:
String(describing: type(of: vc))→ full name (e.g.,MyApp.HomeViewController,UIHostingController<...>)- Strip generic
<...>parameters →UIHostingController - Strip module prefix →
HomeViewController - Returns
nilif the result is an Apple framework base class (UIViewController,UINavigationController,UITabBarController,UIHostingController, etc.) — the label is auto-hidden
→ The text shown in the overlay is always grep-able. Use Open Quickly (⇧⌘O) or grep to jump straight to the file.
A demo app is included in the repository:
- SwiftUI: Basic / Deep Navigation / Sheet / Full-Screen Cover / TabView
- UIKit:
UINavigationController/UITabBarController/ Modal / Container ViewController
Open ScreenNameViewer-For-iOS.xcodeproj and run to see the library in action across each case.
classDiagram
direction TB
class ScreenNameViewer {
<<enum>>
+start(configure)$
+stop()$
}
class Configuration {
<<struct>>
+viewController: LabelStyle
+route: LabelStyle
+verticalPosition: VerticalPosition
}
class LabelStyle {
<<struct>>
+textColor: UIColor
+backgroundColor: UIColor
+textSize: CGFloat
+enabled: Bool
}
class TrackScreenNameModifier {
<<ViewModifier>>
-id: UUID
-routeName: String?
}
class Tracker {
<<MainActor singleton>>
+shared: Tracker$
-isRunning: Bool
+start(config)
+stop()
+handleViewDidAppear(vc)
+handleViewDidDisappear(vc)
+setRoute(id, name)
+removeRoute(id)
}
class VCStack {
<<struct>>
-entries: WeakVC[]
+push(vc)
+remove(vc)
+top: UIViewController?
}
class RouteRegistry {
<<struct>>
-entries: tuples
+set(id, name)
+remove(id)
+current: String?
}
class RenderScheduler {
<<MainActor>>
-scheduled: Bool
+schedule(action)
}
class Swizzler {
<<enum>>
+swizzleOnce()$
}
class VCNameFormatter {
<<enum>>
+names(for: vc)$ Names?
}
class Names {
<<struct>>
+display: String
+full: String
}
class OverlayManager {
<<MainActor>>
+render(vc, route, config)
+removeAll()
+topVisibleViewController(in)$
}
class SceneOverlay {
<<MainActor>>
+update(vc, route, config)
+handlePotentialLabelTap(at, fromWindow)
+tearDown()
}
class OverlayWindow {
<<UIWindow>>
+update(...)
+handlePotentialLabelTap(at)
+hitTest()
}
class OverlayViewController {
<<UIViewController>>
+update(...)
+handlePotentialLabelTap(at)
-showToast(text)
}
class AppWindowTapInstaller {
<<NSObject + UIGestureDelegate>>
+onTap: closure
+installIfNeeded(on: window)
}
Configuration *-- LabelStyle
VCNameFormatter ..> Names
ScreenNameViewer ..> Tracker
TrackScreenNameModifier ..> Tracker
Swizzler ..> Tracker
Tracker *-- VCStack
Tracker *-- RouteRegistry
Tracker *-- RenderScheduler
Tracker *-- OverlayManager
Tracker ..> Swizzler
OverlayManager *-- SceneOverlay
OverlayManager *-- AppWindowTapInstaller
SceneOverlay *-- OverlayWindow
SceneOverlay ..> VCNameFormatter
OverlayWindow *-- OverlayViewController
Notation
*--composition (the parent owns the child instance directly)..>dependency (calls only, no ownership)<<...>>stereotype (struct / enum / MainActor class / UIWindow, etc.)+public,-private,$static
|
Donghyeon Kim |