본문 바로가기
코딩 공부/Swift

[SwiftUI] NavigationView에서 Root뷰로 돌아오기

by JH-M 2023. 3. 17.

NavigationView에서 뒤로 돌아가기 위해서는 왼쪽 상단에 뒤로가기 버튼을 누르거나 dismiss() 메서드를 호출하게 되어 있습니다. 다만 첫번째 Root뷰로 돌아오기 위해서 뒤로가기 버튼을 누르거나 dismiss() 메서드를 여러번 호출해야하는 번거로움이 있습니다. UIKit의 UINavigationViewController에서는 popToRootViewController(animated: true)를 호출해서 한번에 Root뷰로 돌아오는 방법이 있지만 SwiftUI에서는 그 방법을 직접 구현해서 사용해야 합니다.

 

방법1: UINavigationViewController 활용하기

struct NavigationUtil {
  static func popToRootView() {
      let keyWindow = UIApplication.shared.connectedScenes
              .filter({$0.activationState == .foregroundActive})
              .compactMap({$0 as? UIWindowScene})
              .first?.windows
              .filter({$0.isKeyWindow}).first
    findNavigationController(viewController: keyWindow?.rootViewController)?
      .popToRootViewController(animated: true)
  }

  static func findNavigationController(viewController: UIViewController?) -> UINavigationController? {
    guard let viewController = viewController else {
      return nil
    }

    if let navigationController = viewController as? UINavigationController {
      return navigationController
    }

    for childViewController in viewController.children {
      return findNavigationController(viewController: childViewController)
    }

    return nil
  }
}

NavigationUtil에 popToRootView 에 구현된 let keyWindow 는 현재 앱의 키윈도우를 찾기위해 사용됩니다. 키 윈도우는 현재 앱의 활성화 된 윈도우 중 하나이며 사용자 상호 작용을 수신합니다.

 

UIApplication.shared.connectedScenes는 iOS 13 이상에서 사용할 수 있으며, 모든 연결된 UIScene 인스턴스를 반환합니다. filter를 사용하여 현재 활성 상태인 UIScene을 찾습니다. 그런 다음 compactMap을 사용하여 해당 UIScene이 UIWindowScene으로 캐스팅 될 수 있는지 확인합니다. first를 사용하여 첫 번째 UIWindowScene을 가져옵니다. 그런 다음 windows.filter({$0.isKeyWindow}).first를 사용하여 키 윈도우를 찾습니다. 만약 키 윈도우를 찾지 못하면 keyWindow는 nil이 됩니다.

 

iOS 15에서는 Scene 인스턴스의 컬렉션을 가져올 때 새로운 API를 사용해야 하며, iOS 15 미만 버전을 지원하는 앱에서는 기존의 UIApplication.shared.windows 속성을 사용할 수 있습니다.

 

이어서, findNavigationController를 통해서 UINavigationController를 가져와서 .popToRootViewController(animated: true)를 통해 Root뷰로 되돌아갈 수 있습니다.

 

아래 코드는 NavigationUtil을 활용한 예제입니다.

struct ContentView: View {

    var body: some View {
        NavigationView {
            NavigationLink(destination: SecondView()) {
                Text("Go to second view")
            }
            .navigationBarTitle("Root View")
        }
    }
}

struct SecondView: View {

    var body: some View {
        NavigationLink(destination: ThirdView()) {
            Text("Go to third view")
        }
        .navigationBarTitle("Second View")
    }
}

struct ThirdView: View {
    
    var body: some View {
        Button("Go back to Root view") {
            NavigationUtil.popToRootView()
        }
        .navigationBarTitle("Third View")
    }
}

위 코드는 NavigationUtil을 활용하여 세번째 뷰(ThirdView)에서 루트 뷰(ContentView)로 돌아갈 수 있도록 구현하고 있습니다.

 

ContentView는 NavigationView로 감싸져 있으며, "Go to second view" 버튼을 누르면 SecondView로 이동할 수 있는 NavigationLink가 추가되어 있습니다.

SecondView는 또 다른 NavigationLink를 가지고 있으며, "Go to third view" 버튼을 누르면 ThirdView로 이동합니다.

 

ThirdView에서 "Go back to Root view" 버튼을 누르면 NavigationUtil.popToRootView()를 통해 Root뷰로 되돌아 갈 수 있습니다.

 

방법2: View ID 를 변경해서 View다시 그리기

final class AppState : ObservableObject {
    @Published var rootViewId = UUID()
}

AppState 클래스는 ObservableObject 프로토콜을 준수하며, rootViewId라는 이름의 @Published 속성이 있습니다. @Published 속성은 값이 변경될 때마다 뷰에 알리기 위해 사용됩니다.

 

@main
struct MyApp: App {
    @ObservedObject var appState = AppState()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .id(appState.rootViewId)
                .environmentObject(appState)
        }
    }
}

MyApp은 앱의 진입점입니다. @ObservedObject 속성 데코레이터를 사용하여 AppState 인스턴스를 만들고, 이를 앱의 전체 범위에서 사용할 수 있도록 environmentObject 메소드를 사용하여 ContentView에 전달합니다.

 

MyApp은 프로젝트 생성할때 정한 이름에 따라 바뀔 수 있습니다.

 

ContentView는 appState를 @EnvironmentObject로 선언하여 AppState 인스턴스를 참조하고, rootViewId를 사용하여 뷰의 고유 ID를 할당합니다. rootViewId 값이 변경되면 ContentView의 뷰가 다시 그려지게 됩니다.

 

struct ContentView: View {
    
    var body: some View {
        NavigationView {
            NavigationLink(destination: SecondView()) {
                Text("Go to second view")
            }
            .navigationBarTitle("Root View")
        }
    }
}

struct SecondView: View {

    var body: some View {
        NavigationLink(destination: ThirdView()) {
            Text("Go to third view")
        }
        .navigationBarTitle("Second View")
    }
}

struct ThirdView: View {
    @EnvironmentObject var appState: AppState
    
    var body: some View {
        Button("Go back to Root view") {
            appState.rootViewId = UUID()
        }
        .navigationBarTitle("Third View")
    }
}

위 코드는 방법1의 예제 코드에서 NavigationUtil대신 앱 상태 객체(AppState)를 활용하여 세번째 뷰(ThirdView)에서 루트 뷰(ContentView)로 돌아갈 수 있도록 구현하고 있습니다.

 

ThirdView에서는 앱 상태 객체를 사용하기 위해 @EnvironmentObject를 선언하고, "Go back to Root view" 버튼을 누르면 앱 상태 객체의 rootViewId를 새 UUID로 업데이트합니다. 이는 ContentView의 id 속성과 바인딩되어 있으므로, 루트 뷰가 새로 그려지게 됩니다.

 

따라서, ThirdView에서 버튼을 누르면 새로운 루트 뷰(ContentView)로 돌아가는 효과를 볼 수 있습니다.

 

방법2는 Canvas에서 정상적으로 작동하지 않습니다. 아이폰 시뮬레이터를 사용해서 테스트해주세요.

댓글