ScreenNameViewer is a debugging tool that displays the name of the currently visible screen as an overlay.
In UIKit, it shows the currently visible UIViewController name.
In SwiftUI, it can also show the current NavigationStack Route name.
This helps you quickly identify which file the current screen is defined in, improving debugging and development efficiency.
- Real-time screen name display: Shows the current
UIViewControllername and SwiftUINavigationStackRoute in real time - Automatic lifecycle tracking: Tracks the current screen based on the
UIViewControllerlifecycle - DEBUG only: Internal code is wrapped in
#if DEBUG, so it is automatically disabled in RELEASE builds — zero runtime cost - UI customization: Customize text size, color, vertical position, and more
- Memory safe: Prevents memory leaks using weak references and automatic cleanup
- Touch interaction: Tap a label to show the full name in a toast. Non-label areas pass through and never block the underlying app
- Both SwiftUI and UIKit: One library covers both frameworks
In Xcode, open File → Add Package Dependencies... and enter:
https://github.com/DongLab-DevTools/ScreenNameViewer-For-iOSOr add it directly to Package.swift:
dependencies: [
.package(url: "https://github.com/DongLab-DevTools/ScreenNameViewer-For-iOS", from: "{latestVersion}")
]Add it to your target dependencies:
.target(
name: "MyApp",
dependencies: ["ScreenNameViewer"]
)- iOS 16.0 or higher deployment target
- Xcode 15 or higher
- Swift 5.9 or higher
- Call
ScreenNameViewer.install()inAppDelegate. - The class name of the currently visible
UIViewControlleris automatically shown on the left label.
import UIKit
import ScreenNameViewer
@main
final class AppDelegate: UIResponder, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
ScreenNameViewer.install()
return true
}
}- If your app uses the SwiftUI App lifecycle, call
ScreenNameViewer.install()inApp.init().
import SwiftUI
import ScreenNameViewer
@main
struct MyApp: App {
init() {
ScreenNameViewer.install()
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}- Initialization alone enables current screen tracking.
- To display the
NavigationStackRoute name in SwiftUI, add the modifier below. The right label is automatically updated on push/pop.
struct ContentView: View {
@State private var path: [Route] = []
var body: some View {
NavigationStack(path: $path) {
// ...destinations
}
.trackScreenName(path: path)
}
}- If you use
NavigationLink(value:)without a path onNavigationStack, automatic tracking is not possible. - In this case, you can use the wrapper instead of
navigationDestination. - It automatically generates a screen name based on the value received by the destination closure.
NavigationStack {
VStack {
NavigationLink("Go to screen 1", value: "1")
NavigationLink("Go to screen 2", value: "2")
}
.navigationDestinationWithScreenName(for: String.self) { value in
Text("This is screen number \(value)")
}
}- Display example:
ContentView.swift : value: 1
Tip
Adding trackScreenName() in multiple places can increase the scope of changes when the library is removed or updated.
If you’re concerned about tracking library code being scattered across views, we recommend using accessibilityIdentifier to reduce dependency on the tracking library.
This value is not displayed as a label on the screen.
- Screens outside the
NavigationStackpath cannot be tracked automatically. - In this case, you can explicitly declare a name with
.trackScreenName("ScreenName")as needed.
.sheet(isPresented: $showSheet) {
SheetView()
.trackScreenName("StandardSheet")
}
.fullScreenCover(isPresented: $showCover) {
CoverView()
.trackScreenName("FullScreenCover")
}
TabView {
HomeView()
.trackScreenName("Tab.Home")
.tabItem { Label("Home", systemImage: "house") }
}You can customize the overlay style with install { config in ... }.
ScreenNameViewer.install { config in
// Left label — UIViewController name
config.viewController.textColor = .white
config.viewController.backgroundColor = UIColor.black.withAlphaComponent(0.7)
config.viewController.textSize = 12
// 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(viewController) / right(route)
config.verticalPosition = .top
// 4-edge margin from safeArea. Only the top/bottom matching verticalPosition is applied.
config.margin = UIEdgeInsets(top: 4, left: 8, bottom: 4, right: 8)
// Indent child labels by depth (set to false for flat layout)
config.indentByDepth = true
}-
viewController / route: Style for each label
textColor: Text colorbackgroundColor: Background colortextSize: Text sizepaddingHorizontal/paddingVertical: Internal paddingcornerRadius: Corner radius
-
verticalPosition: Vertical position of the overlay (
.top/.bottom)- Horizontal placement is fixed: left(viewController) / right(route)
-
margin: 4-edge
UIEdgeInsetsfrom safeArea. Only top/bottom matchingverticalPositionis applied -
indentByDepth: Whether to indent child labels by depth (default
true)
ScreenNameViewer tracks the current screen information and displays it as debugging labels in the app screen.
Left label
- Displays the current UIKit / SwiftUI View name.
Right label
- Displays the current Route name of SwiftUI
NavigationStack.
- ScreenNameViewer hooks tracking logic into the
viewDidAppear / viewDidDisappearcall timing ofUIViewControllerto track the currently visibleUIViewController. - It then removes generic / module prefixes from the class name and displays a name that is easy to find in user code on the left label.
- SwiftUI screens are hosted through
UIHostingController, so ScreenNameViewer extracts the inner SwiftUI View name and displays it on the left label.
- SwiftUI Route tracking is enabled by declaring
.trackScreenName(path:)onNavigationStack. - When
pathchanges, SwiftUI recomputes the View, and the Route name is updated based on the newpath.last. - The updated Route name is displayed on the right label.
Names shown in the overlay are normalized so they can be searched directly in user code.
-
Get the full name with
String(describing: type(of: vc))
Example:MyApp.HomeViewController,UIHostingController<...> -
Remove generic
<...>parts
Example:UIHostingController<ContentView>→UIHostingController -
Remove module prefixes
Example:MyApp.HomeViewController→HomeViewController -
Filter Apple framework base classes
Example:UIViewController,UINavigationController,UITabBarController,UIHostingController
→ The name shown on screen can be found immediately with grep or Xcode Open Quickly(⇧⌘O).
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 it to see how the library works in each case.
classDiagram
direction TB
class ScreenNameViewer {
<<enum>>
+install(enabled, configure)$
}
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 DisplaySnapshot {
<<struct>>
+viewController: UIViewController?
+vcDisplay: String?
+childDisplays: [String]
+introspectedDisplay: String?
}
class VCStack {
<<struct>>
-entries: WeakVC[]
+push(vc)
+remove(vc)
+top: UIViewController?
+topMap(transform)
}
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>>
+displayName(for: vc)$ String?
}
class SwiftUIIntrospection {
<<enum>>
+extractRootName(from: vc)$ String?
}
class FrameworkModules {
<<enum>>
+names: Set~String~$
+isAppleFrameworkClass(cls)$ Bool
}
class OverlayManager {
<<MainActor>>
+render(snapshot, route, config)
+removeAll()
+topVisibleViewController(in)$
}
class SceneOverlay {
<<MainActor>>
+update(vcDisplay, childDisplays, introspectedDisplay, route, config)
+handlePotentialLabelTap(at, fromWindow)
+tearDown()
}
class OverlayView {
<<UIView>>
+update(...)
+handlePotentialLabelTap(at)
-showToast(text)
-point(inside, with): false
}
class AppWindowTapInstaller {
<<NSObject + UIGestureDelegate>>
+onTap: closure
+installIfNeeded(on: window)
}
Configuration *-- LabelStyle
Tracker *-- DisplaySnapshot
ScreenNameViewer ..> Tracker
TrackScreenNameModifier ..> Tracker
Swizzler ..> Tracker
Tracker *-- VCStack
Tracker *-- RouteRegistry
Tracker *-- RenderScheduler
Tracker *-- OverlayManager
Tracker ..> Swizzler
Tracker ..> VCNameFormatter
Tracker ..> SwiftUIIntrospection
VCNameFormatter ..> FrameworkModules
SwiftUIIntrospection ..> FrameworkModules
OverlayManager *-- SceneOverlay
OverlayManager *-- AppWindowTapInstaller
SceneOverlay *-- OverlayView
Notation
*--composition: the parent directly owns the child instance..>dependency: calls only, no ownership<<...>>stereotype: struct / enum / MainActor class / ViewModifier, etc.+public-private$static
|
Donghyeon Kim |