본문 바로가기

iOS/SwiftUI

SwiftUI NavigationStack을 활용한 상위 뷰에서 루트 뷰 이동 (Pop to Root)

 

 

 🔍 서론

이번에는 iOS17 이상에서 사용되는 NavigationStack을 활용한 pop to root 코드를 구현해 볼 것이다.

 

iOS16 이하에서 사용되는  NavigationView로는 isActive로 pop to root를 구현할 수 있지만, NavigationStack에서는 path를 이용해서 pop to root를 구현한다.

 

이 방식은 처음 보면 복잡하게 느낄 수 있지만, path 관련 코드만 잘 정리해서 작성해 두면 프로젝트 코드 전체 부분에서 깔끔하게 pop to root 기능을 적용할 수 있다.

 

 

 

 📱 NavigationStack Pop to Root

결론부터 말하면, NavigationStack은 path라는 경로에 뷰의 값을 채우면서 스택이 쌓이는데, 이 경로를 모두 없애면 루트 뷰로 돌아가진다.

 

먼저 프로젝트의 구조는 아래와 같다.

 

 

그다음, 뷰들을 컨트롤할 Observable 클래스를 아래와 같이 만들어준다.

@Observable
class NavigationControlTower {
    var paths = NavigationPath()
    
    @ViewBuilder
    func navigate(to view: Views) -> some View {
        switch view {
            
        case .View1:
            View1()
            
        case .View2:
            View2()
            
        case .View3:
            View3()
        }
    }
    
    func push(_ view: Views) {
        paths.append(view)
    }
    
    func pop() {
        paths.removeLast()
    }
    
    func popToRoot() {
        paths.removeLast(paths.count)
    }
}

enum Views {
    case View1
    case View2
    case View3
}

 

위의 클래스는 NavigationStack의 경로로 사용할 path 변수를 가지며, navigate 메서드를 통해 지정한 뷰를 리턴한다.

 

또한 경로를 조작하는 push, pop, popToRoot와 같은 메서드도 구현하였다.

 

 

다음으로 ContentView 코드이다. 

struct ContentView: View {
    @State private var navControlTower = NavigationControlTower()
    
    var body: some View {
        NavigationStack(path: $navControlTower.paths) {
            
            navControlTower.navigate(to: .View1)
                .navigationDestination(for: Views.self) { view in
                    navControlTower.navigate(to: view)
                }
        }
        .environment(navControlTower)
    }
}

 

위의 코드에서는 방금 만든 NavigationControlTower을 상태 클래스로 등록하고, 편의성을 위해 environment를 사용하여 환경 변수에 등록해 준다.

 

또한, path에 아까 구현한 paths를 바인딩하고, 이 ContentView에서는 View1을 내포하고 있으므로 뷰를 paths 경로에 추가하지 않고 반환만 시키는 navigate메서드를 사용하여 반환받을 뷰 이름인 View1을 명시해 준다.

 

이 뷰가 뷰 스택의 시작 부분이므로 navigationDestination을 활용하여 뷰가 paths에 추가될 때 어떻게 처리될지를 정의한다.

 

위에서 구현한 navigationDestination의 동작 과정은 아래와 같다.

 

 

navPush 메서드로 paths에 뷰 추가

 

👇

 

navigationDestination이 paths에 추가된 뷰를 감지하여 매개변수 view에 삽입

 

👇

 

매개변수 view가 navigate 메서드의 인자로 삽입

 

👇

 

navigate 메서드가 삽입된 인자와 일치하는 뷰 반환

 

 

 

참고로, 여기서 ContentView와 같이 navigate 메서드를 바로 사용하면 paths에 경로가 추가되지 않는다. (그냥 화면에 뷰만 띄우는 역할)

 

 

이제 차례로 View1에서 View3까지 나열하겠다.

struct View1: View {
    @Environment(NavigationControlTower.self) var navControlTower: NavigationControlTower
    
    var body: some View {
        Text("View 1")
            .font(.title)
            .padding(.bottom, 50)
        
        Button {
            navControlTower.push(.View2)
        } label: {
            Text("View2로 이동")
        }
    }
}

 

struct View2: View {
    @Environment(NavigationControlTower.self) var navControlTower: NavigationControlTower
    
    var body: some View {
        Text("View 2")
            .font(.title)
            .padding(.bottom, 50)
        
        Button {
            navControlTower.push(.View3)
        } label: {
            Text("View3로 이동")
        }
    }
}

 

struct View3: View {
    @Environment(NavigationControlTower.self) var navControlTower: NavigationControlTower
    
    var body: some View {
        Text("View 3")
            .font(.title)
            .padding(.bottom, 50)
        
        Button {
            navControlTower.popToRoot()
        } label: {
            Text("루트 뷰로 이동")
        }
    }
}

 

각 뷰에서 다음 뷰로 넘어가기 위해 NavigationLink를 사용하는 것이 아니라, paths에 경로를 넣으면서 뷰를 띄우기 위해 Button을 사용하였다.

 

마지막 뷰에서는 popToRoot() 메서드를 호출하면 paths에 있는 모든 경로가 다 지워지면서 루트 뷰로 돌아간다.

 

 

 

실행 영상