ScreenNameViewer는 현재 표시 중인 화면의 이름을 오버레이로 보여주는 디버깅 도구입니다.
UIKit에서는 현재 표시 중인 UIViewController 이름을, SwiftUI에서는 NavigationStack의 Route 이름까지 함께 확인할 수 있습니다.
이를 통해 현재 화면이 어떤 파일에 정의되어 있는지 빠르게 파악할 수 있어 디버깅과 개발 효율을 높여줍니다.
- 실시간 화면 이름 표시: 현재 표시 중인
UIViewController이름과 SwiftUINavigationStackRoute를 화면에 실시간 표시 - 자동 라이프사이클 추적:
UIViewController의 lifecycle을 기반으로 현재 화면 자동 추적 - DEBUG 전용: 내부 코드가
#if DEBUG로 감싸져 있어 RELEASE 빌드에서는 자동 비활성화 — 런타임 비용 0 - UI 커스터마이징: 텍스트 크기, 색상, 수직 위치 등 자유롭게 설정 가능
- 메모리 안전: 약한 참조 + 자동 정리로 메모리 누수 방지
- 터치 상호작용: 라벨 터치 시 전체 이름을 토스트로 표시, 그 외 영역은 모두 통과 — 기존 화면의 터치 막지 않음
- SwiftUI / UIKit 모두 지원: 한 라이브러리로 두 프레임워크 동시 커버
Xcode에서 File → Add Package Dependencies... 다이얼로그에 다음 URL 입력:
https://github.com/DongLab-DevTools/ScreenNameViewer-For-iOS또는 Package.swift의 dependencies에 직접 추가:
dependencies: [
.package(url: "https://github.com/DongLab-DevTools/ScreenNameViewer-For-iOS", from: "1.0.0")
]타겟의 dependencies에도 추가:
.target(
name: "MyApp",
dependencies: ["ScreenNameViewer"]
)- iOS 16.0 이상 deployment target
- Xcode 15 이상
- Swift 5.9 이상
AppDelegate에서ScreenNameViewer.install()을 호출합니다.- 좌측 라벨에 현재 표시 중인
UIViewController의 클래스명이 자동으로 표시됩니다.
import UIKit
import ScreenNameViewer
@main
final class AppDelegate: UIResponder, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
ScreenNameViewer.install()
return true
}
}- SwiftUI App lifecycle을 사용하는 경우,
App.init()에서ScreenNameViewer.install()을 호출합니다.
import SwiftUI
import ScreenNameViewer
@main
struct MyApp: App {
init() {
ScreenNameViewer.install()
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}- 초기화만으로도 현재 화면에 대한 추적은 동작합니다.
- SwiftUI에서
NavigationStack의 Route 이름까지 표시하려면 아래 modifier를 추가합니다. 이후 push/pop 시 우측 라벨이 자동 갱신됩니다.
struct ContentView: View {
@State private var path: [Route] = []
var body: some View {
NavigationStack(path: $path) {
// ...destinations
}
.trackScreenName(path: path)
}
}NavigationStack에 path 없이NavigationLink(value:)를 사용하는 경우에는 자동 추적이 불가능합니다.- 이 경우
navigationDestination대신 wrapper를 사용할 수 있습니다. - destination closure가 받은 value를 기준으로 화면 이름을 자동 생성합니다.
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)")
}
}- 노출 예시:
ContentView.swift : value: 1
Tip
trackScreenName()을 여러 곳에 추가하면 라이브러리 제거 또는 업데이트 시 수정 범위가 커질 수 있습니다.
라이브러리 코드가 뷰 전반에 퍼지는 것이 부담된다면 추적 라이브러리에 대한 의존을 줄일 수 있는 accessibilityIdentifier 사용을 권장합니다. (화면에 라벨로 표시되진 않습니다.)
NavigationStackpath 밖에 있는 화면은 자동 추적이 불가능합니다.- 이 경우 필요에 따라
.trackScreenName("화면이름")을 명시적으로 선언할 수 있습니다.
.sheet(isPresented: $showSheet) {
SheetView()
.trackScreenName("StandardSheet")
}
.fullScreenCover(isPresented: $showCover) {
CoverView()
.trackScreenName("FullScreenCover")
}
TabView {
HomeView()
.trackScreenName("Tab.Home")
.tabItem { Label("Home", systemImage: "house") }
}install { config in ... }로 오버레이 스타일을 커스터마이징할 수 있습니다.
ScreenNameViewer.install { config in
// 좌측 라벨 — UIViewController 이름
config.viewController.textColor = .white
config.viewController.backgroundColor = UIColor.black.withAlphaComponent(0.7)
config.viewController.textSize = 12
// 우측 라벨 — NavigationStack Route
config.route.textColor = .systemYellow
config.route.backgroundColor = UIColor.black.withAlphaComponent(0.7)
config.route.textSize = 12
// 수직 위치: top / bottom
// 수평 위치는 좌측(viewController) / 우측(route) 고정
config.verticalPosition = .top
// safeArea 기준 4방향 margin. top/bottom 은 verticalPosition 에 맞는 쪽만 적용
config.margin = UIEdgeInsets(top: 4, left: 8, bottom: 4, right: 8)
// child 라벨에 depth 들여쓰기 적용 (false 면 평면 표시)
config.indentByDepth = true
}-
viewController / route: 두 라벨 각각의 스타일
textColor: 텍스트 색상backgroundColor: 배경 색상textSize: 텍스트 크기paddingHorizontal/paddingVertical: 내부 패딩cornerRadius: 모서리 둥글기
-
verticalPosition: 오버레이의 수직 위치 (
.top/.bottom)- 수평 위치는 좌측(viewController) / 우측(route) 고정
-
margin: safeArea 기준 4방향
UIEdgeInsets. top/bottom 은verticalPosition에 맞는 쪽만 적용 -
indentByDepth: child 라벨 depth 들여쓰기 적용 여부 (기본
true)
ScreenNameViewer는 현재 화면 정보를 추적하고, 이를 디버깅용 라벨로 앱 화면에 표시합니다.
좌측 라벨
- 현재 화면의 UIKit / SwiftUI View 이름이 표시됩니다.
우측 라벨
- SwiftUI
NavigationStack의 현재 Route 이름이 표시됩니다.
UIViewController의viewDidAppear / viewDidDisappear호출 시점에 추적 로직을 함께 실행하도록 연결하여 현재 보이는UIViewController를 추적합니다.- 이후 클래스명에서 generic / module prefix를 정리하고, 사용자 코드에서 찾기 쉬운 이름만 좌측 라벨에 표시합니다.
- SwiftUI 화면은
UIHostingController를 통해 호스팅되므로, 내부 SwiftUI View 이름을 추출해 좌측 라벨에 표시합니다.
- SwiftUI Route는
NavigationStack에.trackScreenName(path:)를 선언하여 추적합니다. path가 변경되면 SwiftUI가 View를 다시 그리고, 새path.last기준으로 Route 이름이 갱신됩니다.- 갱신된 Route 이름은 우측 라벨에 표시됩니다.
오버레이에 표시되는 이름은 사용자 코드에서 바로 검색할 수 있도록 정규화됩니다.
-
String(describing: type(of: vc))→ 전체 이름 획득
예:MyApp.HomeViewController,UIHostingController<...> -
generic
<...>제거
예:UIHostingController<ContentView>→UIHostingController -
module prefix 제거
예:MyApp.HomeViewController→HomeViewController -
Apple framework 기본 클래스는 필터링
예:UIViewController,UINavigationController,UITabBarController,UIHostingController
→ 화면에 보이는 이름은 grep 또는 Xcode Open Quickly(⇧⌘O)로 바로 찾을 수 있습니다.
레포 내부에 데모 앱이 포함되어 있습니다.
- SwiftUI: Basic / Deep Navigation / Sheet / Full-Screen Cover / TabView
- UIKit:
UINavigationController/UITabBarController/ Modal / Container ViewController
ScreenNameViewer-For-iOS.xcodeproj를 열고 실행하시면 각 케이스에서 라이브러리가 어떻게 동작하는지 확인할 수 있습니다.
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
표기 의미
*--컴포지션: 부모가 자식 인스턴스를 직접 보유..>의존: 호출만 하고 소유하지 않음<<...>>스테레오타입: struct / enum / MainActor class / ViewModifier 등+public-private$static
|
Donghyeon Kim |