SwiftUI는 매우 직관적이고 선언적인 UI 구성 방식을 제공하지만, iOS 14 기준으로 제공되는 네비게이션 도구에는 아직 제약이 있어 화면 전환을 보다 유연하게 제어하는 데 어려움을 겪을 수 있습니다. 특히, 복잡한 네비게이션 구조를 필요로 하거나 UIKit에서 제공하는 네비게이션 기능을 활용하고 싶을 때 SwiftUI의 NavigationView
와 NavigationLink
는 부족한 면이 있습니다.
SwiftUI를 통해 프로젝트를 진행하던 중 화면 전환의 어려움을 겪었고, 결국 UIKit의 UINavigationController
를 활용해 SwiftUI와 UIKit 네비게이션을 조합하여 구현하는 방식을 도입하게 되었습니다. 이 글에서는 UINavigationController
를 SwiftUI에서 사용하는 방법과 최상단 뷰 컨트롤러를 찾아 새로운 화면을 표시하는 방식에 대해 설명드리겠습니다.
1. UIKit의 UINavigationController를 SwiftUI에서 사용하는 방법
먼저, UIKit의 UINavigationController
를 SwiftUI에 통합하기 위해 UIViewControllerRepresentable
을 사용하여 UIKit 네비게이션 컨트롤러를 SwiftUI로 감싸는 작업이 필요합니다. UIViewControllerRepresentable
은 SwiftUI와 UIKit 간의 상호 작용을 가능하게 해주는 프로토콜로, UIKit 뷰 컨트롤러를 SwiftUI에 맞게 재구성할 수 있도록 합니다.
UINavigationView 구조체 생성
UIKit의 UINavigationController
를 감싸기 위한 UINavigationView
라는 간단한 구조체를 작성합니다. 이 구조체는 UIViewControllerRepresentable
을 준수하고, NavigationManager
라는 클래스를 활용해 화면 전환을 제어합니다.
import SwiftUI
import UIKit
struct UINavigationView: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UINavigationController {
let rootViewController = UIHostingController(rootView: RootView())
let navigationController = UINavigationController(rootViewController: rootViewController)
// 싱글톤 NavigationManager에 navigationController를 주입
NavigationManager.shared.navigationController = navigationController
return navigationController
}
func updateUIViewController(_ uiViewController: UINavigationController, context: Context) {
// 필요할 경우 업데이트 로직 구현
}
}
NavigationManager 클래스 정의
화면 전환을 제어하기 위해 NavigationManager
라는 클래스를 만들어 줍니다. 이 클래스는 ObservableObject
를 준수하며, 두 가지 push
메서드를 제공하여 SwiftUI의 View
와 UIKit의 UIViewController
모두를 유연하게 화면 전환할 수 있도록 합니다.
import UIKit
import SwiftUI
class NavigationManager: ObservableObject {
static let shared = NavigationManager() // 싱글톤 인스턴스 생성
weak var navigationController: UINavigationController?
private init() {} // 외부에서 인스턴스 생성 방지
// SwiftUI View를 받는 push 함수
func push<V: View>(_ view: V) {
let hostingController = UIHostingController(rootView: view)
navigationController?.pushViewController(hostingController, animated: true)
}
// UIKit UIViewController를 받는 push 함수
func push(_ viewController: UIViewController) {
navigationController?.pushViewController(viewController, animated: true)
}
func pop() {
navigationController?.popViewController(animated: true)
}
}
이제 SwiftUI에서 NavigationManager
의 push
메서드를 통해 SwiftUI와 UIKit 스타일의 화면 전환을 모두 구현할 수 있습니다.
2. SwiftUI에서 화면 전환 제어하기
NavigationManager
와 UINavigationView
를 설정한 후, SwiftUI 뷰에서 UIKit 네비게이션을 통해 새로운 화면으로 전환할 수 있습니다.
RootView와 NextSwiftUIView 구현
RootView
에서는 버튼을 통해 새로운 화면으로 전환하며, SwiftUI 뷰를 위한 push
메서드와 UIKit UIViewController
를 위한 push
메서드를 모두 사용할 수 있습니다.
struct RootView: View {
var body: some View {
VStack {
Text("Root View")
// SwiftUI View를 push
Button(action: {
NavigationManager.shared.push(NextSwiftUIView())
}) {
Text("Go to Next SwiftUI View")
}
// UIKit ViewController를 push
Button(action: {
let nextViewController = UIViewController()
nextViewController.view.backgroundColor = .systemBlue
NavigationManager.shared.push(nextViewController)
}) {
Text("Go to Next UIKit ViewController")
}
}
}
}
struct NextSwiftUIView: View {
var body: some View {
VStack {
Text("Next SwiftUI View")
Button(action: {
NavigationManager.shared.pop()
}) {
Text("Back")
}
}
}
}
ContentView에서 UINavigationView 사용하기
SwiftUI의 ContentView
에서는 UINavigationView
를 호출하고, NavigationManager
를 환경 객체로 전달합니다.
struct ContentView: View {
@StateObject private var navigationManager = NavigationManager()
var body: some View {
UINavigationView()
.edgesIgnoringSafeArea(.all)
}
}
이제 SwiftUI의 버튼에서 UIKit의 UINavigationController
를 통해 화면 전환을 쉽게 할 수 있습니다.
3. 최상단에 있는 ViewController를 찾아 모달로 화면 띄우기
SwiftUI 프로젝트에서 특정 상황에 따라 최상단에 있는 뷰 컨트롤러를 탐색해 새로운 화면을 모달로 띄워야 할 때가 있습니다. 이를 위해 UIApplication
의 확장으로 최상단 뷰 컨트롤러를 탐색하는 코드를 작성할 수 있습니다.
최상단 ViewController 찾기 (findRootViewController)
아래 확장 코드는 UIApplication
의 루트 뷰 컨트롤러부터 시작해 현재 최상단에 있는 UIViewController
를 찾아 반환합니다.
extension UIApplication {
class func findRootViewController(base: UIViewController? = UIApplication.shared.connectedScenes
.compactMap { ($0 as? UIWindowScene)?.keyWindow }
.first?.rootViewController) -> UIViewController? {
if let nav = base as? UINavigationController {
return findRootViewController(base: nav.visibleViewController)
}
if let tab = base as? UITabBarController, let selected = tab.selectedViewController {
return findRootViewController(base: selected)
}
if let presented = base?.presentedViewController {
return findRootViewController(base: presented)
}
return base
}
}
최상단 ViewController에서 새로운 화면 띄우기
SwiftUI 뷰에서 이 함수를 통해 현재 최상단에 있는 뷰 컨트롤러를 찾고, 그 위에 새로운 뷰를 띄울 수 있습니다.
import SwiftUI
func presentNewView() {
if let topController = UIApplication.findRootViewController() {
let newSwiftUIView = UIHostingController(rootView: NewSwiftUIView())
newSwiftUIView.modalPresentationStyle = .fullScreen // 스타일 설정 가능
topController.present(newSwiftUIView, animated: true, completion: nil)
}
}
struct NewSwiftUIView: View {
var body: some View {
VStack {
Text("New View")
Button("Dismiss") {
UIApplication.findRootViewController()?.dismiss(animated: true, completion: nil)
}
}
}
}
이제 presentNewView()
함수를 호출하면 최상단의 뷰 컨트롤러에 NewSwiftUIView
가 모달로 표시됩니다.
마무리
SwiftUI와 UIKit을 함께 사용하여 화면 전환을 구현하는 방법을 살펴보았습니다. SwiftUI의 기본 네비게이션 기능에 제한을 느끼신다면, UIKit의 UINavigationController
와 같은 기존의 네비게이션 도구를 활용하는 것도 좋은 해결책이 될 수 있습니다. 이와 같은 방법을 통해 SwiftUI의 선언적 UI와 UIKit의 강력한 네비게이션 기능을 조합하여 유연한 화면 전환을 구현할 수 있습니다.
'iOS > SwiftUI' 카테고리의 다른 글
SwiftUI에서 Combine을 활용해서 화면 회전 감지하기 (0) | 2024.11.10 |
---|---|
SwiftUI에서 각 탭마다 독립적인 네비게이션 스택 유지하기 (0) | 2024.11.08 |
SwiftUI에서 RestAPI를 통해 서버의 데이터를 화면에 보여주기 (1) | 2024.11.07 |