최근 SwiftUI 프로젝트에서 UIKit으로 구현한 뷰를 추가하기 위해 UIViewControllerRepresentable 타입을 구현한 경험이 있다.
단순히 뷰 구현은 크게 어렵지 않고 잘 진행이 되었는데, 화면전환을 관리하면서 문제가 발생했다.
메인이 SwiftUI이기 때문에 화면 전환은 NavigationStack을 사용하여 진행한다.
UIKit으로 구현한 뷰 내부에서 push나 pop을 했을 때, 특정 화면에서는 네비게이션 바가 표시되면 안되었는데, 네비게이션의 주체가 SwiftUI로 되어있다보니 어떻게 컨트롤하면 좋을지 어려웠다.
화면 제어는 Coordinator가 담당하고 있었기 때문에 이를 이용할까도 싶었지만, 학습을 하며 구조를 파악하니 그렇게 하지 않아도 쉽게 해결할 수 있다는 것을 알게되어서 다른 방법을 사용했다.
이번에는 직접 경험한 네비게이션 바 전이 문제와 백버튼 레이아웃 이슈, 그리고 그 해결 과정을 정리한다.
... (More) 버튼이 표시됨.Life Cycle 메서드를 통한 명시적 상태 동기화 및 backBarButtonItem 타이틀 초기화.메인 화면(SwiftUI)에서는 네비게이션 바를 숨기고, 상세 화면(UIKit 기반)에서는 보여줘야 하는 상황이었다. 하지만 메인에서 toolbar(.hidden)을 설정했음에도 상세 화면으로 이동했을 때 바가 나타나지 않거나, 반대로 상세에서 돌아왔을 때 메인 화면에 빈 바가 생기는 등 상태가 꼬이는 현상이 발생했다.
상세 화면으로 진입했을 때, 당연히 보여야 할 뒤로가기 화살표와 이전 타이틀 대신 세 개의 점(...) 혹은 More 버튼이 나타났다. 이는 네비게이션 스택이 중첩되면서 시스템이 레이아웃 우선순위를 제대로 판단하지 못해 발생하는 문제였다.
SwiftUI의 선언적 방식(toolbar)에만 의존하지 말고, UIViewControllerRepresentable 내부의 UIViewController에서 직접 네비게이션 바 상태를 핸들링한다.
final class DetailViewController: UIViewController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// 상세 화면 진입 시 네비게이션 바를 명시적으로 노출
self.navigationController?.setNavigationBarHidden(false, animated: animated)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// 이전 화면(Main)으로 돌아갈 때 다시 숨김 처리
self.navigationController?.setNavigationBarHidden(true, animated: animated)
}
}
시스템이 이전 화면의 타이틀 길이를 계산하다가 실패하면 ...을 보여준다. 이를 방지하기 위해 이전 화면(Main)의 navigationItem에서 백버튼 타이틀을 빈 값으로 명시하여 공간 점유율을 최소화하고 우선순위를 높인다.
// SwiftUI 메인 뷰에서 이전 화면의 백버튼 설정을 제어하기 어려울 때,
// Representable의 makeUIViewController 단계에서 처리 가능
func makeUIViewController(context: Context) -> UINavigationController {
let viewController = DetailViewController()
// 이전 화면의 타이틀을 빈 값으로 설정하여 '...' 현상 방지
let backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
viewController.navigationItem.backBarButtonItem = backBarButtonItem
return viewController
}
| 구분 | 문제 발생 시 | 해결 후 |
|---|---|---|
| 네비게이션 바 | 화면 전환 시 사라지거나 멈춤 | 진입/이탈 시점에 맞춰 정확히 토글됨 |
| 백버튼 형태 | ... (Ellipsis) 표시 | 깨끗한 뒤로가기 화살표 표시 |
| 제어 방식 | SwiftUI 선언부에 의존 | UIKit 생명주기를 통한 명령형 제어 병행 |
SwiftUI는 매우 편리하고 선언적이지만, UIKit과 섞이는 순간 그 경계선에서는 SwiftUI의 편리함이 통하지 않는 영역이 생긴다.
때문에 둘을 함께 사용할 때의 구조적인 내용이나 각 프레임워크의 특성을 제대로 이해해야만 보다 효과적이고 효율적인 코드를 작성할 수 있다.
NavigationStack은 결국 내부적으로 UINavigationController를 사용한다.viewWillAppear) 사라지는(viewWillDisappear) 시점의 명령형 코드가 때로는 가장 확실한 해결책이 된다.backBarButtonItem처럼 모호할 수 있는 부분은 직접 정의해주는 것이 안전하다.하이브리드 구조에서 발생하는 네비게이션 이슈는 대부분 "제어권의 주체"가 모호해서 발생한다. SwiftUI의 선언적 편리함은 유지하되, UIViewControllerRepresentable 내부에서는 UIKit의 규칙을 철저히 따르는 것이 트러블슈팅의 핵심이다.
Environment 값과 UIKit 상태를 동기화하는 게 정말 까다롭다. 가능하다면 네비게이션 로직은 한쪽 프레임워크로 통일하는 것이 정신 건강에 이로울 것 같다.