SwiftUI의 TabView는 손쉽게 탭 구조를 만들 수 있게 해주지만, 기본적으로 각 탭의 네비게이션 스택을 독립적으로 유지하지 않습니다. 예를 들어, 첫 번째 탭에서 여러 화면을 이동한 뒤 다른 탭으로 전환했다가 돌아오면 네비게이션 스택이 초기화됩니다.
이 글에서는 UIKit의 UINavigationController를 활용하여 각 탭마다 독립적인 네비게이션 스택을 유지하는 방법을 소개합니다. 이를 통해 사용자가 각 탭에서 스택 상태를 유지하며 화면을 이동할 수 있도록 합니다.
프로젝트 구성
MVVM 패턴을 사용하여 프로젝트를 다음과 같이 구성했습니다.
YourProject
├── Managers
│ └── NavigationManager.swift // 각 탭의 UINavigationController 스택을 관리하는 매니저
├── ViewModels
│ └── TabViewModel.swift // 각 탭의 네비게이션 로직을 관리하는 ViewModel
├── Views
│ ├── ContentView.swift // 메인 TabView
│ ├── HomeView.swift // 홈 탭 뷰
│ ├── SettingsView.swift // 설정 탭 뷰
│ ├── ProfileView.swift // 프로필 탭 뷰
│ └── DetailView.swift // 네비게이션으로 이동하는 상세 뷰
이 구조는 각 탭의 스택 상태를 독립적으로 관리하여 각 탭이 자신만의 네비게이션 흐름을 유지하도록 구성되어 있습니다.
1. NavigationManager 정의
SwiftUI에서는 UIKit의 UINavigationController를 직접 사용할 수 없기 때문에, NavigationManager 클래스를 만들어 UIKit의 네비게이션 로직을 통합 관리합니다. 각 탭에서 독립적인 네비게이션 스택을 유지하기 위해 UINavigationController 인스턴스를 관리하고, 화면 전환 로직을 제어하는 메서드를 제공합니다.
import SwiftUI
import UIKit
class NavigationManager: NSObject, ObservableObject {
private var navigationController: UINavigationController?
func setNavigationController(_ navigationController: UINavigationController) {
self.navigationController = navigationController
}
func push<Content: View>(_ view: Content) {
guard let navigationController = navigationController else { return }
let viewController = UIHostingController(rootView: view)
navigationController.pushViewController(viewController, animated: true)
}
func pop() {
navigationController?.popViewController(animated: true)
}
}
이 클래스는 네비게이션 스택을 관리하며, SwiftUI의 ViewModel에서 쉽게 호출할 수 있는 push
와 pop
메서드를 제공합니다.
2. 각 탭의 ViewModel 설정
TabViewModel을 사용하여 NavigationManager와 함께 각 탭의 네비게이션 로직을 관리합니다. 이렇게 하면 각 탭의 네비게이션 흐름을 독립적으로 유지할 수 있습니다.
class TabViewModel: ObservableObject {
@Published var navigationManager = NavigationManager()
func navigateToDetail(with text: String) {
navigationManager.push(DetailView(text: text))
}
}
TabViewModel은 화면 전환 로직을 포함하여 NavigationManager를 통해 UIKit의 네비게이션을 제어합니다.
3. ContentView에서 TabView 구성
ContentView에서는 TabView와 NavigationManager를 사용해 각각의 탭이 독립적인 네비게이션 스택을 유지하도록 설정합니다. SwiftUI의 UIViewControllerRepresentable을 사용해 NavigationManager를 각 탭에 연결합니다.
struct ContentView: View {
@StateObject private var homeViewModel = TabViewModel()
@StateObject private var settingsViewModel = TabViewModel()
@StateObject private var profileViewModel = TabViewModel()
var body: some View {
TabView {
NavigationManagerRepresentable(manager: homeViewModel.navigationManager, rootView: HomeView(viewModel: homeViewModel))
.tabItem {
Label("홈", systemImage: "house.fill")
}
NavigationManagerRepresentable(manager: settingsViewModel.navigationManager, rootView: SettingsView(viewModel: settingsViewModel))
.tabItem {
Label("설정", systemImage: "gearshape.fill")
}
NavigationManagerRepresentable(manager: profileViewModel.navigationManager, rootView: ProfileView(viewModel: profileViewModel))
.tabItem {
Label("프로필", systemImage: "person.fill")
}
}
.accentColor(.blue)
}
}
각 탭은 NavigationManagerRepresentable을 통해 독립적인 NavigationManager 인스턴스를 가지며, 이를 통해 각 탭의 네비게이션 스택이 독립적으로 관리됩니다.
4. 각 View에서 화면 전환 처리
HomeView, SettingsView, ProfileView는 TabViewModel을 통해 화면 전환을 처리합니다. 예를 들어, HomeView에서는 navigateToDetail
메서드를 호출하여 DetailView로 이동합니다.
struct HomeView: View {
@ObservedObject var viewModel: TabViewModel
var body: some View {
VStack {
Text("홈 화면")
.font(.largeTitle)
.foregroundColor(.white)
Button(action: {
viewModel.navigateToDetail(with: "홈 상세 화면")
}) {
Text("상세 화면으로 이동")
.foregroundColor(.blue)
.padding()
.background(Color.white)
.cornerRadius(10)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.green)
.navigationTitle("홈")
}
}
각각의 탭은 독립적인 NavigationManager를 사용하므로, 첫 번째 탭에서 화면을 이동한 후 두 번째 탭으로 전환했다가 다시 돌아오더라도 첫 번째 탭의 네비게이션 스택이 그대로 유지됩니다.
5. 상세 화면 구현
각 탭에서 화면 전환 시 도달하는 DetailView는 단일 뷰로 작성하여 각 탭에서 재사용할 수 있도록 구성했습니다.
struct DetailView: View {
let text: String
var body: some View {
Text(text)
.font(.title)
.foregroundColor(.white)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.gray)
.navigationTitle("상세 화면")
}
}
각 탭의 스택 독립성을 위한 핵심 포인트
- NavigationManager 활용: UINavigationController 인스턴스를 각 탭마다 독립적으로 관리하여, 탭 전환 시에도 스택 상태가 초기화되지 않도록 유지합니다.
- MVVM 패턴: NavigationManager를 ViewModel에서 관리함으로써 뷰와 네비게이션 로직을 분리하고, 재사용성과 확장성을 높였습니다.
- 각 탭에 NavigationManagerRepresentable 연결: TabView에서 각 탭에 대해 별도의 NavigationManager 인스턴스를 사용하도록 연결해줌으로써, 각 탭의 네비게이션 흐름을 독립적으로 관리합니다.
'iOS > SwiftUI' 카테고리의 다른 글
SwiftUI에서 Combine을 활용해서 화면 회전 감지하기 (0) | 2024.11.10 |
---|---|
SwiftUI에서 UIKit의 `UINavigationController`로 화면 전환 구현하기 (0) | 2024.11.07 |
SwiftUI에서 RestAPI를 통해 서버의 데이터를 화면에 보여주기 (1) | 2024.11.07 |