SwiftUI: как открыть Root View

Наконец, теперь с бета-версией 5 мы можем программно перейти к родительскому представлению. Однако в моем приложении есть несколько мест, в которых в представлении есть кнопка "Сохранить", которая завершает несколько этапов процесса и возвращается в начало. В UIKit я использую popToRootViewController(), но я не смог найти способ сделать то же самое в SwiftUI.

Ниже приведен простой пример схемы, которую я пытаюсь достичь. Есть идеи?

import SwiftUI

struct DetailViewB: View {
    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
    var body: some View {
        VStack {
            Text("This is Detail View B.")

            Button(action: { self.presentationMode.value.dismiss() } )
            { Text("Pop to Detail View A.") }

            Button(action: { /* How to do equivalent to popToRootViewController() here?? */ } )
            { Text("Pop two levels to Master View.") }

        }
    }
}

struct DetailViewA: View {
    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
    var body: some View {
        VStack {
            Text("This is Detail View A.")

            NavigationLink(destination: DetailViewB() )
            { Text("Push to Detail View B.") }

            Button(action: { self.presentationMode.value.dismiss() } )
            { Text("Pop one level to Master.") }
        }
    }
}

struct MasterView: View {
    var body: some View {
        VStack {
            Text("This is Master View.")

            NavigationLink(destination: DetailViewA() )
            { Text("Push to Detail View A.") }
        }
    }
}

struct ContentView: View {
    var body: some View {
        NavigationView {
            MasterView()
        }
    }
}

33 ответа

Решение

Я провел последние часы, чтобы попытаться решить ту же проблему. Насколько я вижу, нелегкий способ сделать это с текущей бета-версией 5. Единственный способ, который я нашел, очень хакерский, но работает. В основном добавьте издателя к вашему DetailViewA, который будет запущен из DetailViewB. В DetailViewB закрывают представление и сообщают издателю, который сам закроет DetailViewA.

    struct DetailViewB: View {
    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
    var publisher = PassthroughSubject<Void, Never>()

    var body: some View {
        VStack {
            Text("This is Detail View B.")

            Button(action: { self.presentationMode.value.dismiss() } )
            { Text("Pop to Detail View A.") }

            Button(action: {
                DispatchQueue.main.async {
                self.presentationMode.wrappedValue.dismiss()
                self.publisher.send()
                }
            } )
            { Text("Pop two levels to Master View.") }

        }
    }
}

struct DetailViewA: View {
    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
    var publisher = PassthroughSubject<Void, Never>()

    var body: some View {
        VStack {
            Text("This is Detail View A.")

            NavigationLink(destination: DetailViewB(publisher:self.publisher) )
            { Text("Push to Detail View B.") }

            Button(action: { self.presentationMode.value.dismiss() } )
            { Text("Pop one level to Master.") }
        }
        .onReceive(publisher, perform: { _ in
            DispatchQueue.main.async {
                print("Go Back to Master")
                self.presentationMode.wrappedValue.dismiss()
            }
        })
    }
}

[ОБНОВЛЕНИЕ] Я все еще работаю над этим, поскольку на последней бета-версии 6 до сих пор нет решения.

Я нашел другой способ вернуться к корню, но на этот раз я теряю анимацию и иду прямо к корню. Идея заключается в принудительном обновлении корневого представления, что приводит к очистке стека навигации.

Но в конечном итоге только Apple может предложить правильное решение, так как управление навигационным стеком недоступно в SwiftUI.

NB. Простое решение с помощью уведомлений ниже работает на iOS, а не на watchOS, поскольку watchOS очищает корневой вид из памяти после 2 уровня навигации. Но наличие внешнего класса, управляющего состоянием для watchOS, должно просто работать.

struct DetailViewB: View {
    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>

    @State var fullDissmiss:Bool = false
    var body: some View {
        SGNavigationChildsView(fullDissmiss: self.fullDissmiss){
            VStack {
                Text("This is Detail View B.")

                Button(action: { self.presentationMode.wrappedValue.dismiss() } )
                { Text("Pop to Detail View A.") }

                Button(action: {
                    self.fullDissmiss = true
                } )
                { Text("Pop two levels to Master View with SGGoToRoot.") }
            }
        }
    }
}

struct DetailViewA: View {
    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>

    @State var fullDissmiss:Bool = false
    var body: some View {
        SGNavigationChildsView(fullDissmiss: self.fullDissmiss){
            VStack {
                Text("This is Detail View A.")

                NavigationLink(destination: DetailViewB() )
                { Text("Push to Detail View B.") }

                Button(action: { self.presentationMode.wrappedValue.dismiss() } )
                { Text("Pop one level to Master.") }

                Button(action: { self.fullDissmiss = true } )
                { Text("Pop one level to Master with SGGoToRoot.") }
            }
        }
    }
}

struct MasterView: View {
    var body: some View {
        VStack {
            Text("This is Master View.")
            NavigationLink(destination: DetailViewA() )
            { Text("Push to Detail View A.") }
        }
    }
}

struct ContentView: View {

    var body: some View {
        SGRootNavigationView{
            MasterView()
        }
    }
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
#endif

struct SGRootNavigationView<Content>: View where Content: View {
    let cancellable = NotificationCenter.default.publisher(for: Notification.Name("SGGoToRoot"), object: nil)

    let content: () -> Content

    init(@ViewBuilder content: @escaping () -> Content) {
        self.content = content
    }

    @State var goToRoot:Bool = false

    var body: some View {
        return
            Group{
            if goToRoot == false{
                NavigationView {
                content()
                }
            }else{
                NavigationView {
                content()
                }
            }
            }.onReceive(cancellable, perform: {_ in
                DispatchQueue.main.async {
                    self.goToRoot.toggle()
                }
            })
    }
}

struct SGNavigationChildsView<Content>: View where Content: View {
    let notification = Notification(name: Notification.Name("SGGoToRoot"))

    var fullDissmiss:Bool{
        get{ return false }
        set{ if newValue {self.goToRoot()} }
    }

    let content: () -> Content

    init(fullDissmiss:Bool, @ViewBuilder content: @escaping () -> Content) {
        self.content = content
        self.fullDissmiss = fullDissmiss
    }

    var body: some View {
        return Group{
            content()
        }
    }

    func goToRoot(){
        NotificationCenter.default.post(self.notification)
    }
}

Установка модификатора представления isDetailLink к false на NavigationLink это ключ к тому, чтобы заставить работать pop-to-root. isDetailLink является trueпо умолчанию и адаптируется к содержащему его представлению. Например, в альбомной ориентации iPad разделенный вид разделен иisDetailLinkгарантирует, что вид назначения будет показан с правой стороны. НастройкаisDetailLink к falseследовательно, это означает, что целевое представление всегда будет помещено в стек навигации; таким образом всегда можно снять.

Наряду с настройкой isDetailLink к false на NavigationLink, пройти isActiveпривязка к каждому последующему виду назначения. Наконец, когда вы захотите перейти к корневому представлению, установите значение наfalse и он автоматически все отключит:

import SwiftUI

struct ContentView: View {
    @State var isActive : Bool = false

    var body: some View {
        NavigationView {
            NavigationLink(
                destination: ContentView2(rootIsActive: self.$isActive),
                isActive: self.$isActive
            ) {
                Text("Hello, World!")
            }
            .isDetailLink(false)
            .navigationBarTitle("Root")
        }
    }
}

struct ContentView2: View {
    @Binding var rootIsActive : Bool

    var body: some View {
        NavigationLink(destination: ContentView3(shouldPopToRootView: self.$rootIsActive)) {
            Text("Hello, World #2!")
        }
        .isDetailLink(false)
        .navigationBarTitle("Two")
    }
}

struct ContentView3: View {
    @Binding var shouldPopToRootView : Bool

    var body: some View {
        VStack {
            Text("Hello, World #3!")
            Button (action: { self.shouldPopToRootView = false } ){
                Text("Pop to root")
            }
        }.navigationBarTitle("Three")
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Определенно, @malhal имеет ключ к решению, но для меня нецелесообразно передавать Binding в View в качестве параметров. Как указал @Imthath, окружающая среда - намного лучший способ.

Вот еще один подход, который смоделирован после того, как Apple опубликовал метод dismiss() для перехода к предыдущему представлению.

Определите расширение среды:

struct RootPresentationModeKey: EnvironmentKey {
    static let defaultValue: Binding<RootPresentationMode> = .constant(RootPresentationMode())
}

extension EnvironmentValues {
    var rootPresentationMode: Binding<RootPresentationMode> {
        get { return self[RootPresentationModeKey.self] }
        set { self[RootPresentationModeKey.self] = newValue }
    }
}

typealias RootPresentationMode = Bool

extension RootPresentationMode {

    public mutating func dismiss() {
        self.toggle()
    }
}

ПРИМЕНЕНИЕ:

  1. Добавьте модификатор представления.environment к корневому NavigationView, чтобы установить значение ключа.rootPresentationMode для привязки Bool, используемой для представления первого дочернего представления.

    .environment (.rootPresentationMode, self. $isActive)

  2. Добавьте оболочку свойства @Environment к любому представлению, которое захочет вернуться в корневое представление.

    @Environment(.rootPresentationMode) private var rootPresentationMode: привязка

  3. Наконец, вызов метода dismiss() для обернутого значения @Environment var приведет к появлению корневого представления.

    self.rootPresentationMode.wrappedValue.dismiss()

Я опубликовал полный рабочий пример на GitHub:

https://github.com/Whiffer/SwiftUI-PopToRootExample

Поскольку в настоящее время SwiftUI по-прежнему использует UINavigationController в фоновом режиме, также можно вызвать его popToRootViewController(animated:)функция. Вам нужно только выполнить поиск UINavigationController в иерархии контроллеров представления следующим образом:

      struct NavigationUtil {
  static func popToRootView() {
    findNavigationController(viewController: UIApplication.shared.windows.filter { $0.isKeyWindow }.first?.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
  }
}

И используйте это так:

      struct ContentView: View {
    var body: some View {
      NavigationView { DummyView(number: 1) }
    }
}

struct DummyView: View {
  let number: Int

  var body: some View {
    VStack(spacing: 10) {
      Text("This is view \(number)")
      NavigationLink(destination: DummyView(number: number + 1)) {
        Text("Go to view \(number + 1)")
      }
      Button(action: { NavigationUtil.popToRootView() }) {
        Text("Or go to root view!")
      }
    }
  }
}

Дамы и господа, представляем решение Apple для этой самой проблемы. * также представлен вам через HackingWithSwift (который я украл из lol): под программной навигацией

(Проверено на Xcode 12 и iOS 14)

по сути вы используете tag и selection внутри navigationlink чтобы перейти прямо на любую страницу, которую вы хотите.

      struct ContentView: View {
@State private var selection: String? = nil

var body: some View {
    NavigationView {
        VStack {
            NavigationLink(destination: Text("Second View"), tag: "Second", selection: $selection) { EmptyView() }
            NavigationLink(destination: Text("Third View"), tag: "Third", selection: $selection) { EmptyView() }
            Button("Tap to show second") {
                self.selection = "Second"
            }
            Button("Tap to show third") {
                self.selection = "Third"
            }
        }
        .navigationBarTitle("Navigation")
    }
}
}

Вы можете использовать @environmentobject введен в ContentView() для обработки выбора:

      class NavigationHelper: ObservableObject {
    @Published var selection: String? = nil
}

ввести в приложение:

      @main
struct YourApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView().environmentObject(NavigationHelper())
        }
    }
}

и используйте это:

      struct ContentView: View {
@EnvironmentObject var navigationHelper: NavigationHelper

var body: some View {
    NavigationView {
        VStack {
            NavigationLink(destination: Text("Second View"), tag: "Second", selection: $navigationHelper.selection) { EmptyView() }
            NavigationLink(destination: Text("Third View"), tag: "Third", selection: $navigationHelper.selection) { EmptyView() }
            Button("Tap to show second") {
                self.navigationHelper.selection = "Second"
            }
            Button("Tap to show third") {
                self.navigationHelper.selection = "Third"
            }
        }
        .navigationBarTitle("Navigation")
    }
}
}

Чтобы вернуться к просмотру содержимого в дочерних навигационных ссылках, вы просто устанавливаете navigationHelper.selection = nil.

Обратите внимание, что вам даже не нужно использовать тег и выбор для последующих дочерних навигационных ссылок, если вы не хотите - у них не будет функциональности для перехода к этой конкретной навигационной ссылке.

Решение iOS 16

Теперь, наконец, вы можете перейти к корневому представлению с недавно добавленнымNavigationStack!!!

      struct DataObject: Identifiable, Hashable {
    let id = UUID()
    let name: String
}

@available(iOS 16.0, *)
struct ContentView8: View {
    @State private var path = NavigationPath()
    
    var body: some View {
        NavigationStack(path: $path) {
            Text("Root Pop")
                .font(.largeTitle)
                .foregroundColor(.primary)
            
            NavigationLink("Click Item", value: DataObject.init(name: "Item"))
            
            .listStyle(.plain)
            .navigationDestination(for: DataObject.self) { course in
                Text(course.name)
                NavigationLink("Go Deeper", value: DataObject.init(name: "Item"))
                Button("Back to root") {
                    path = NavigationPath()
                }
            }
        }
        .padding()
    }
}

Я нашел простое решение, чтобы перейти к корневому представлению. Я отправляю уведомление, а затем слушаю уведомление об изменении идентификатора NavigationView, это обновит NavigationView. Анимации нет, но выглядит неплохо. Вот пример:

      @main
struct SampleApp: App {
    @State private var navigationId = UUID()
    
    var body: some Scene {
        WindowGroup {
            NavigationView {
                Screen1()
            }
            .id(navigationId)
            .onReceive(NotificationCenter.default.publisher(for: Notification.Name("popToRootView"))) { output in
                navigationId = UUID()
            }
        }
    }
}

struct Screen1: View {
    var body: some View {
        VStack {
            Text("This is screen 1")
            NavigationLink("Show Screen 2", destination: Screen2())            
        }
    }
}

struct Screen2: View {
    var body: some View {
        VStack {
            Text("This is screen 2")
            Button("Go to Home") {
                NotificationCenter.default.post(name: Notification.Name("popToRootView"), object: nil)
            }            
        }
    }
}

Это обновление для @x0randgat3ответ, который работает для нескольких NavigationViewsв пределах TabView.

      struct NavigationUtil {
  static func popToRootView() {
    findNavigationController(viewController: UIApplication.shared.windows.filter { $0.isKeyWindow }.first?.rootViewController)?
      .popToRootViewController(animated: true)
  }

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

    if let navigationController = viewController as? UITabBarController {
      return findNavigationController(viewController: navigationController.selectedViewController)
    }

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

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

    return nil
  }
}

Это заняло некоторое время, но я понял, как использовать сложную навигацию в swiftui. Хитрость заключается в том, чтобы собрать все состояния ваших представлений, которые показывают, отображаются ли они.

Начните с определения NavigationController. Я добавил выбор для вкладки tabview и логические значения, указывающие, отображается ли конкретное представление

import SwiftUI
final class NavigationController: ObservableObject  {

  @Published var selection: Int = 1

  @Published var tab1Detail1IsShown = false
  @Published var tab1Detail2IsShown = false

  @Published var tab2Detail1IsShown = false
  @Published var tab2Detail2IsShown = false
}

настройка tabview с двумя вкладками и привязка нашего NavigationController.selection к tabview:

import SwiftUI
struct ContentView: View {

  @EnvironmentObject var nav: NavigationController

  var body: some View {

    TabView(selection: self.$nav.selection){

            FirstMasterView() 
            .tabItem {
                 Text("First")
            }
            .tag(0)

           SecondMasterView() 
            .tabItem {
                 Text("Second")
            }
            .tag(1)
        }
    }
}

В качестве примера это один navigationStacks

import SwiftUI


struct FirstMasterView: View {

    @EnvironmentObject var nav: NavigationController

   var body: some View {
      NavigationView{
        VStack{

          NavigationLink(destination: FirstDetailView(), isActive: self.$nav.tab1Detail1IsShown) {
                Text("go to first detail")
            }
        } .navigationBarTitle(Text("First MasterView"))
     }
  }
}

struct FirstDetailView: View {

   @EnvironmentObject var nav: NavigationController
   @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>

 var body: some View {

    VStack(spacing: 20) {
        Text("first detail View").font(.title)


        NavigationLink(destination: FirstTabLastView(), isActive: self.$nav.tab1Detail2IsShown) {
            Text("go to last detail on nav stack")
        }

        Button(action: {
            self.nav.tab2Detail1IsShown = false //true will go directly to detail
            self.nav.tab2Detail2IsShown = false 

            self.nav.selection = 1
        }) { Text("Go to second tab")
        }
    }
        //in case of collapsing all the way back
        //there is a bug with the environment object
        //to go all the way back I have to use the presentationMode
        .onReceive(self.nav.$tab1Detail2IsShown, perform: { (out) in
            if out ==  false {
                 self.presentationMode.wrappedValue.dismiss()
            }
        })
    }
 }


struct FirstTabLastView: View {
   @EnvironmentObject var nav: NavigationController

   var body: some View {
       Button(action: {
           self.nav.tab1Detail1IsShown = false
           self.nav.tab1Detail2IsShown = false
       }) {Text("Done and go back to beginning of navigation stack")
       }
   }
}

Я надеюсь, что смогу объяснить подход, который полностью ориентирован на состояние SwiftUI.

Это решение основано на ответе Малхала, использует предложения Имтата и Флорина Одагиу и требует видео Пола Хадсона NavigationView, чтобы собрать все это для меня. Идея очень проста. Для параметра isActive в navigationLink устанавливается значение true при нажатии. Это позволяет появиться второму виду. Вы можете использовать дополнительные ссылки, чтобы добавить больше просмотров. Чтобы вернуться в корень, просто установите isActive в false. Второй вид, а также любые другие, которые могли быть сложены, исчезнут.

import SwiftUI

class Views: ObservableObject {
    @Published var stacked = false
}

struct ContentView: View {
    @ObservedObject var views = Views()
    
    var body: some View {
        NavigationView {
            NavigationLink(destination: ContentView2(), isActive: self.$views.stacked) {
                Text("Go to View 2") //Tapping this link sets stacked to true
            }
            .isDetailLink(false)
            .navigationBarTitle("ContentView")
        }
        .environmentObject(views) //Inject a new views instance into the navigation view environment so that it's available to all views presented by the navigation view. 
    }
}

struct ContentView2: View {
    
    var body: some View {
        NavigationLink(destination: ContentView3()) {
            Text("Go to View 3")
        }
        .isDetailLink(false)
        .navigationBarTitle("View 2")
    }
}

struct ContentView3: View {
    @EnvironmentObject var views: Views
    
    var body: some View {
        
        Button("Pop to root") {
            self.views.stacked = false //By setting this to false, the second view that was active is no more. Which means, the content view is being shown once again.
        }
        .navigationBarTitle("View 3")
    }
}

Для меня, чтобы получить полный контроль над навигацией, которая все еще отсутствует в swiftUI, я просто встроил SwiftUI View в UINavigationController. внутриSceneDelegate. Обратите внимание, что я скрываю панель навигации, чтобы использовать NavigationView в качестве дисплея.

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {

        UINavigationBar.appearance().tintColor = .black

        let contentView = OnBoardingView()
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            let hostingVC = UIHostingController(rootView: contentView)
            let mainNavVC = UINavigationController(rootViewController: hostingVC)
            mainNavVC.navigationBar.isHidden = true
            window.rootViewController = mainNavVC
            self.window = window
            window.makeKeyAndVisible()
        }
    }
}

И затем я создал этот протокол и расширение, HasRootNavigationController

import SwiftUI
import UIKit

protocol HasRootNavigationController {
    var rootVC:UINavigationController? { get }

    func push<Content:View>(view: Content, animated:Bool)
    func setRootNavigation<Content:View>(views:[Content], animated:Bool)
    func pop(animated: Bool)
    func popToRoot(animated: Bool)
}

extension HasRootNavigationController where Self:View {

    var rootVC:UINavigationController? {
        guard let scene = UIApplication.shared.connectedScenes.first,
            let sceneDelegate = scene as? UIWindowScene,
            let rootvc = sceneDelegate.windows.first?.rootViewController
                as? UINavigationController else { return nil }
        return rootvc
    }

    func push<Content:View>(view: Content, animated:Bool = true) {
        rootVC?.pushViewController(UIHostingController(rootView: view), animated: animated)
    }

    func setRootNavigation<Content:View>(views: [Content], animated:Bool = true) {
        let controllers =  views.compactMap { UIHostingController(rootView: $0) }
        rootVC?.setViewControllers(controllers, animated: animated)
    }

    func pop(animated:Bool = true) {
        rootVC?.popViewController(animated: animated)
    }

    func popToRoot(animated: Bool = true) {
        rootVC?.popToRootViewController(animated: animated)
    }
}

После этого в моем представлении SwiftUI я использовал / реализовал HasRootNavigationController протокол и расширение

extension YouSwiftUIView:HasRootNavigationController {

    func switchToMainScreen() {
        self.setRootNavigation(views: [MainView()])
    }

    func pushToMainScreen() {
         self.push(view: [MainView()])
    }

    func goBack() {
         self.pop()
    }

    func showTheInitialView() {
         self.popToRoot()
    }
}

вот суть моего кода на случай, если у меня появятся какие-то обновления. https://gist.github.com/michaelhenry/945fc63da49e960953b72bbc567458e6

Спасибо "Малхал" за ваше решение @Binding. Мне не хватало.isDetailLink(false)модификатор. Что я узнал из вашего кода.

В моем случае я не хочу использовать @Binding при каждом последующем просмотре.

Итак, это мое решение, в котором я использую EnvironmentObject.

Шаг 1. Создайте AppState ObservableObject

import SwiftUI
import Combine

class AppState: ObservableObject {
    @Published var moveToDashboard: Bool = false
}

Шаг 2. Создайте экземпляр AppState и добавить в contentView в SceneDelegate

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        // Create the SwiftUI view that provides the window contents.
        let contentView = ContentView()
        let appState = AppState()

        // Use a UIHostingController as window root view controller.
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = UIHostingController(rootView:
                contentView
                    .environmentObject(appState)
            )
            self.window = window
            window.makeKeyAndVisible()
        }
    }

Шаг 3: Код ContentView.swift Итак, я обновляю appState значение последнего представления в стеке, которое использует .onReceive() Я захватываю в contentView, чтобы обновить isActive значение false для NavigationLink.

Ключевым моментом здесь является использование .isDetailLink(false)с помощью NavigationLink. Иначе не получится.

import SwiftUI
import Combine

class AppState: ObservableObject {
    @Published var moveToDashboard: Bool = false
}

struct ContentView: View {
    @EnvironmentObject var appState: AppState
    @State var isView1Active: Bool = false

    var body: some View {
        NavigationView {
            VStack {
                Text("Content View")
                    .font(.headline)

                NavigationLink(destination: View1(), isActive: $isView1Active) {
                    Text("View 1")
                        .font(.headline)
                }
                .isDetailLink(false)
            }
            .onReceive(self.appState.$moveToDashboard) { moveToDashboard in
                if moveToDashboard {
                    print("Move to dashboard: \(moveToDashboard)")
                    self.isView1Active = false
                    self.appState.moveToDashboard = false
                }
            }
        }
    }
}

// MARK:- View 1
struct View1: View {

    var body: some View {
        VStack {
            Text("View 1")
                .font(.headline)
            NavigationLink(destination: View2()) {
                Text("View 2")
                    .font(.headline)
            }
        }
    }
}

// MARK:- View 2
struct View2: View {
    @EnvironmentObject var appState: AppState

    var body: some View {
        VStack {
            Text("View 2")
                .font(.headline)
            Button(action: {
                self.appState.moveToDashboard = true
            }) {
                Text("Move to Dashboard")
                .font(.headline)
            }
        }
    }
}


struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Вот мое решение, работает везде, без зависимости.

      let window = UIApplication.shared.connectedScenes
  .filter { $0.activationState == .foregroundActive }
  .map { $0 as? UIWindowScene }
  .compactMap { $0 }
  .first?.windows
  .filter { $0.isKeyWindow }
  .first
let nvc = window?.rootViewController?.children.first as? UINavigationController
nvc?.popToRootViewController(animated: true)

В iOS15 есть простое решение для этого, используя dismiss () и передав dismiss в подпредставление:

      struct ContentView: View {
    @State private var showingSheet = false
    var body: some View {
        NavigationView {
            Button("show sheet", action: { showingSheet.toggle()})
                .navigationTitle("ContentView")
        }.sheet(isPresented: $showingSheet) { FirstSheetView() }
    }
}

struct FirstSheetView: View {
    @Environment(\.dismiss) var dismiss
    var body: some View {
        NavigationView {
            List {
                NavigationLink(destination: SecondSheetView(dismiss: _dismiss) ) {
                    Text("show 2nd Sheet view")
                }
                NavigationLink(destination: ThirdSheetView(dismiss: _dismiss) ) {
                    Text("show 3rd Sheet view")
                }
                Button("cancel", action: {dismiss()} )
            } .navigationTitle("1. SheetView")
        }
    }
}

struct SecondSheetView: View {
    @Environment(\.dismiss) var dismiss
    var body: some View {
            List {
                NavigationLink(destination: ThirdSheetView(dismiss: _dismiss) ) {
                    Text("show 3rd SheetView")
                }
                Button("cancel", action: {dismiss()} )
            } .navigationTitle("2. SheetView")
    }
}

struct ThirdSheetView: View {
    @Environment(\.dismiss) var dismiss
    var body: some View {
            List {
                Button("cancel", action: {dismiss()} )
            } .navigationTitle("3. SheetView")
    }
}

Вот мое медленное, анимированное, немного грубое обратное поп-решение с использованием onAppear, действительное для XCode 11 и iOS 13.1:


import SwiftUI
import Combine


struct NestedViewLevel3: View {
    @Binding var resetView:Bool
    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>

    var body: some View {
        VStack {
            Spacer()
            Text("Level 3")
            Spacer()
            Button(action: {
                self.presentationMode.wrappedValue.dismiss()
            }) {
                Text("Back")
                    .padding(.horizontal, 15)
                    .padding(.vertical, 2)
                    .foregroundColor(Color.white)
                    .clipped(antialiased: true)
                    .background(
                        RoundedRectangle(cornerRadius: 20)
                            .foregroundColor(Color.blue)
                            .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 40, alignment: .center)
                )}
            Spacer()
            Button(action: {
                self.$resetView.wrappedValue = true
                self.presentationMode.wrappedValue.dismiss()
            }) {
                Text("Reset")
                    .padding(.horizontal, 15)
                    .padding(.vertical, 2)
                    .foregroundColor(Color.white)
                    .clipped(antialiased: true)
                    .background(
                        RoundedRectangle(cornerRadius: 20)
                            .foregroundColor(Color.blue)
                            .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 40, alignment: .center)
                )}
            Spacer()
        }
        .navigationBarBackButtonHidden(false)
        .navigationBarTitle("Level 3", displayMode: .inline)
        .onAppear(perform: {print("onAppear level 3")})
        .onDisappear(perform: {print("onDisappear level 3")})

    }
}

struct NestedViewLevel2: View {
    @Binding var resetView:Bool
    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>

    var body: some View {
        VStack {
            Spacer()
            NavigationLink(destination: NestedViewLevel3(resetView:$resetView)) {
                Text("To level 3")
                    .padding(.horizontal, 15)
                    .padding(.vertical, 2)
                    .foregroundColor(Color.white)
                    .clipped(antialiased: true)
                    .background(
                        RoundedRectangle(cornerRadius: 20)
                            .foregroundColor(Color.gray)
                            .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 40, alignment: .center)
                )
                    .shadow(radius: 10)
            }
            Spacer()
            Text("Level 2")
            Spacer()
            Button(action: {
                self.presentationMode.wrappedValue.dismiss()
            }) {
                Text("Back")
                    .padding(.horizontal, 15)
                    .padding(.vertical, 2)
                    .foregroundColor(Color.white)
                    .clipped(antialiased: true)
                    .background(
                        RoundedRectangle(cornerRadius: 20)
                            .foregroundColor(Color.blue)
                            .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 40, alignment: .center)
                )}
            Spacer()
        }
        .navigationBarBackButtonHidden(false)
        .navigationBarTitle("Level 2", displayMode: .inline)
        .onAppear(perform: {
            print("onAppear level 2")
            if self.$resetView.wrappedValue {
                self.presentationMode.wrappedValue.dismiss()
            }
        })
        .onDisappear(perform: {print("onDisappear level 2")})
    }
}

struct NestedViewLevel1: View {
    @Binding var resetView:Bool
    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>

    var body: some View {
        VStack {
            Spacer()
            NavigationLink(destination: NestedViewLevel2(resetView:$resetView)) {
                Text("To level 2")
                    .padding(.horizontal, 15)
                    .padding(.vertical, 2)
                    .foregroundColor(Color.white)
                    .clipped(antialiased: true)
                    .background(
                        RoundedRectangle(cornerRadius: 20)
                            .foregroundColor(Color.gray)
                            .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 40, alignment: .center)
                )
                    .shadow(radius: 10)
            }
            Spacer()
            Text("Level 1")
            Spacer()
            Button(action: {
                self.presentationMode.wrappedValue.dismiss()
            }) {
                Text("Back")
                    .padding(.horizontal, 15)
                    .padding(.vertical, 2)
                    .foregroundColor(Color.white)
                    .clipped(antialiased: true)
                    .background(
                        RoundedRectangle(cornerRadius: 20)
                            .foregroundColor(Color.blue)
                            .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 40, alignment: .center)
                )}
            Spacer()
        }
        .navigationBarBackButtonHidden(false)
        .navigationBarTitle("Level 1", displayMode: .inline)
        .onAppear(perform: {
            print("onAppear level 1")
            if self.$resetView.wrappedValue {
                self.presentationMode.wrappedValue.dismiss()
            }
        })
        .onDisappear(perform: {print("onDisappear level 1")})
    }
}

struct RootViewLevel0: View {
    @Binding var resetView:Bool
    var body: some View {
        NavigationView {
        VStack {
            Spacer()
            NavigationLink(destination: NestedViewLevel1(resetView:$resetView)) {
            Text("To level 1")
                .padding(.horizontal, 15)
                .padding(.vertical, 2)
                .foregroundColor(Color.white)
                .clipped(antialiased: true)
                .background(
                    RoundedRectangle(cornerRadius: 20)
                    .foregroundColor(Color.gray)
                    .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 40, alignment: .center)
                )
                .shadow(radius: 10)
        }
            //.disabled(false)
            //.hidden()
            Spacer()

            }
    }
        //.frame(width:UIScreen.main.bounds.width,height:  UIScreen.main.bounds.height - 110)
        .navigationBarTitle("Root level 0", displayMode: .inline)
        .navigationBarBackButtonHidden(false)
        .navigationViewStyle(StackNavigationViewStyle())
        .onAppear(perform: {
            print("onAppear root level 0")
            self.resetNavView()
        })
        .onDisappear(perform: {print("onDisappear root level 0")})

    }

    func resetNavView(){
        print("resetting objects")
        self.$resetView.wrappedValue = false
    }

}


struct ContentView: View {
    @State var resetView = false
    var body: some View {
        RootViewLevel0(resetView:$resetView)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

NavigationViewKithttps://github.com/fatbobman/NavigationViewKit

      import NavigationViewKit
NavigationView {
            List(0..<10) { _ in
                NavigationLink("abc", destination: DetailView())
            }
        }
        .navigationViewManager(for: "nv1", afterBackDo: {print("back to root") })

в любом представлении в NavigationView

      @Environment(\.navigationManager) var nvmanager         

Button("back to root view") {
    nvmanager.wrappedValue.popToRoot(tag:"nv1"){
             print("other back")
           }
}

Вы также можете вызвать его через NotificationCenter, не вызывая его в представлении.

      let backToRootItem = NavigationViewManager.BackToRootItem(tag: "nv1", animated: false, action: {})
NotificationCenter.default.post(name: .NavigationViewManagerBackToRoot, object: backToRootItem)

Это очень трудно достичь с помощью и NavigationLink. Однако, если вы используете библиотеку https://github.com/canopas/UIPilot , которая является крошечной оболочкой вокруг NavigationView, выскочить в любой пункт назначения очень просто.

Предположим, у вас есть маршруты

      enum AppRoute: Equatable {
    case Home
    case Detail
    case NestedDetail
}

и у вас есть настроенный корневой вид, как показано ниже

      struct ContentView: View {
    @StateObject var pilot = UIPilot(initial: AppRoute.Home)
    
    var body: some View {
        UIPilotHost(pilot)  { route in
            switch route {
                case .Home: return AnyView(HomeView())
                case .Detail: return AnyView(DetailView())
                case .NestedDetail: return AnyView(NestedDetail())
            }
        }
    }
}

И вы хотите поп к Homeиз NestedDetailэкран, просто используйте popToфункция.

      struct NestedDetail: View {
    @EnvironmentObject var pilot: UIPilot<AppRoute>
    
    var body: some View {
        VStack {
            Button("Go to home", action: {
                pilot.popTo(.Home)   // Pop to home
            })
        }.navigationTitle("Nested detail")
    }
}

Сначала я использовал решение от user899918 , которое было опубликовано

Но я столкнулся с проблемой, когда это решение не сработало в моем случае. Это было связано со случаем, когда корневое представление является начальной точкой для двух и более потоков и в какой-то момент этих потоков у пользователя есть возможность сделать . И в этом случае здесь .это решение не сработало, потому что оно имеет одно общее состояние @Environment(\.rootPresentationMode) private var rootPresentationMode

Я сделал с дополнительным перечислением Routeкоторый описывает некоторый конкретный поток, в котором пользователь имеет возможность выполнять pop to root

Менеджер маршрутов:

      final class RouteManager: ObservableObject {
    @Published
    private var routers: [Int: Route] = [:]
    
    subscript(for route: Route) -> Route? {
        get {
            routers[route.rawValue]
        }
        set {
            routers[route.rawValue] = route
        }
    }
    
    func select(_ route: Route) {
        routers[route.rawValue] = route
    }
    
    func unselect(_ route: Route) {
        routers[route.rawValue] = nil
    }
}

Маршрут:

      enum Route: Int, Hashable {
    case signUp
    case restorePassword
    case orderDetails
}

Применение:

      struct ContentView: View {
    @EnvironmentObject
    var routeManager: RouteManager
    
    var body: some View {
        NavigationView {
            VStack {
                NavigationLink(
                    destination: SignUp(),
                    tag: .signUp,
                    selection: $routeManager[for: .signUp]
                ) { EmptyView() }.isDetailLink(false)
                NavigationLink(
                    destination: RestorePassword(),
                    tag: .restorePassword,
                    selection: $routeManager[for: .restorePassword]
                ) { EmptyView() }.isDetailLink(false)
                Button("Sign Up") {
                    routeManager.select(.signUp)
                }
                Button("Restore Password") {
                    routeManager.select(.restorePassword)
                }
            }
            .navigationBarTitle("Navigation")
            .onAppear {
                routeManager.unselect(.signUp)
                routeManager.unselect(.restorePassword)
            }
        }.navigationViewStyle(StackNavigationViewStyle())
    }
}

!! ВАЖНЫЙ !!

Вы должны использовать unselectметод RouteManagerкогда пользователь идет вперед к потоку, а затем возвращается, нажав на кнопку «Назад». В этом случае необходимо сбросить состояние нашего менеджера маршрутов для ранее выбранных потоков, чтобы избежать неопределенного (неожиданного) поведения:

      .onAppear {
    routeManager.unselect(.signUp)
    routeManager.unselect(.restorePassword)
}

Полный демонстрационный проект вы можете найти здесь

I found a solution that works fine for me. here is how it works:

a gif shows how it works

in the ContentView.swift file:

  1. define a class, declare an @EnvironmentObject of RootSelection to record the tag of the current active only in root view.
  2. add a modifier .isDetailLink(false) to each that is not a final detail view.
  3. use a file system hierarchy to simulate the NavigationView.
  4. this solution works fine when the root view has multiple NavigationLink.
      import SwiftUI

struct ContentView: View {
    var body: some View {
        NavigationView {
            SubView(folder: rootFolder)
        }
    }
}

struct SubView: View {
    @EnvironmentObject var rootSelection: RootSelection
    var folder: Folder
    
    var body: some View {
        List(self.folder.documents) { item in
            if self.folder.documents.count == 0 {
                Text("empty folder")
            } else {
                if self.folder.id == rootFolder.id {
                    NavigationLink(item.name, destination: SubView(folder: item as! Folder), tag: item.id, selection: self.$rootSelection.tag)
                        .isDetailLink(false)
                } else {
                    NavigationLink(item.name, destination: SubView(folder: item as! Folder))
                        .isDetailLink(false)
                }
            }
        }
        .navigationBarTitle(self.folder.name, displayMode: .large)
        .listStyle(SidebarListStyle())
        .overlay(
            Button(action: {
                rootSelection.tag = nil
            }, label: {
                Text("back to root")
            })
            .disabled(self.folder.id == rootFolder.id)
        )
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
            .environmentObject(RootSelection())
    }
}

class RootSelection: ObservableObject {
    @Published var tag: UUID? = nil
}

class Document: Identifiable {
    let id = UUID()
    var name: String
    
    init(name: String) {
        self.name = name
    }
}

class File: Document {}

class Folder: Document {
    var documents: [Document]
    
    init(name: String, documents: [Document]) {
        self.documents = documents
        super.init(name: name)
    }
}

let rootFolder = Folder(name: "root", documents: [
    Folder(name: "folder1", documents: [
        Folder(name: "folder1.1", documents: []),
        Folder(name: "folder1.2", documents: []),
    ]),
    Folder(name: "folder2", documents: [
        Folder(name: "folder2.1", documents: []),
        Folder(name: "folder2.2", documents: []),
    ])
])

.environmentObject(RootSelection()) is required for the ContentView() object in xxxApp.swift file

      import SwiftUI

@main
struct DraftApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(RootSelection())
        }
    }
}

Недавно я создал проект с открытым исходным кодом под названием swiftui-navigation-stack(https://github.com/biobeats/swiftui-navigation-stack). Это альтернативный стек навигации для SwiftUI. Взгляните на README, чтобы узнать все подробности, он действительно прост в использовании.

Прежде всего, если вы хотите перемещаться между экранами (например, в полноэкранном режиме), определите свой собственный простой Screen Посмотреть:

struct Screen<Content>: View where Content: View {
    let myAppBackgroundColour = Color.white
    let content: () -> Content

    var body: some View {
        ZStack {
            myAppBackgroundColour.edgesIgnoringSafeArea(.all)
            content()
        }
    }
} 

Затем вставьте свой корень в NavigationStackView (как и со стандартным NavigationView):

struct RootView: View {
    var body: some View {
        NavigationStackView {
            Homepage()
        }
    }
}

Теперь давайте создадим пару дочерних представлений, чтобы показать вам основное поведение:

struct Homepage: View {
    var body: some View {
        Screen {
            PushView(destination: FirstChild()) {
                Text("PUSH FORWARD")
            }
        }
    }
}

struct FirstChild: View {
    var body: some View {
        Screen {
            VStack {
                PopView {
                    Text("JUST POP")
                }
                PushView(destination: SecondChild()) {
                    Text("PUSH FORWARD")
                }
            }
        }
    }
}

struct SecondChild: View {
    var body: some View {
        Screen {
            VStack {
                PopView {
                    Text("JUST POP")
                }
                PopView(destination: .root) {
                    Text("POP TO ROOT")
                }
            }
        }
    }
}

Вы можете использовать PushView а также PopViewперемещаться вперед и назад. Конечно, ваше представление содержимого внутриSceneDelegate должно быть:

// Create the SwiftUI view that provides the window contents.
let contentView = RootView()

Результат:

Ответ @malhal определенно правильный. Я сделал оболочку, которая позволяет мне применять любые модификаторы, которые мне нужны, кроме isDetailLink(false)один и записывать любые данные, которые мне нужны. В частности, он фиксирует isActiveпривязка или tagпривязка, чтобы я мог сбросить их, когда я хочу перейти к любому представлению, объявленному корнем. Параметр isRoot = trueсохранит привязку для этого представления, а dismissПараметр принимает необязательное закрытие на случай, если вам нужно что-то сделать, когда произойдет всплывающее окно. Я скопировал базовые подписи из SwiftUI NavigationLinks для простой навигации на основе логических значений или тегов, чтобы можно было легко редактировать существующие варианты использования. При необходимости должно быть просто добавить другие. Обертка выглядит так:

      struct NavigationStackLink<Label, Destination> : View where Label : View, Destination : View  {
var isActive: Binding<Bool>? // Optionality implies whether tag or Bool binding is used
var isRoot: Bool = false
let link: NavigationLink<Label,Destination>

private var dismisser: () -> Void = {}

/// Wraps [NavigationLink](https://developer.apple.com/documentation/swiftui/navigationlink/init(isactive:destination:label:))
/// `init(isActive: Binding<Bool>, destination: () -> Destination, label: () -> Label)`
/// - Parameters:
///     - isActive:  A Boolean binding controlling the presentation state of the destination
///     - isRoot: Indicate if this is the root view. Used to pop to root level. Default `false`
///     - dismiss: A closure that is called when the link destination is about to be dismissed
///     - destination: The link destination view
///     - label: The links label
init(isActive: Binding<Bool>, isRoot : Bool = false, dismiss: @escaping () -> Void = {}, @ViewBuilder destination: @escaping () -> Destination, @ViewBuilder label: @escaping () -> Label) {
    self.isActive = isActive
    self.isRoot = isRoot
    self.link = NavigationLink(isActive: isActive, destination: destination, label: label)
    self.dismisser = dismiss
}

/// Wraps [NavigationLink ](https://developer.apple.com/documentation/swiftui/navigationlink/init(tag:selection:destination:label:))
init<V>(tag: V, selection: Binding<V?>, isRoot : Bool = false, dismiss: @escaping () -> Void = {}, @ViewBuilder destination: @escaping () -> Destination, @ViewBuilder label: @escaping () -> Label) where V : Hashable
{

    self.isRoot = isRoot
    self.link = NavigationLink(tag: tag, selection: selection, destination: destination, label: label)
    self.dismisser = dismiss
    self.isActive = Binding (get: {
        selection.wrappedValue == tag
    }, set: { newValue in
        if newValue {
          selection.wrappedValue = tag
        } else {
          selection.wrappedValue = nil
        }
    })
}

// Make sure you inject your external store into your view hierarchy
@EnvironmentObject var viewRouter: ViewRouter
var body: some View {
    // Store whatever you need to in your external object
    if isRoot {
        viewRouter.root = isActive
    }
    viewRouter.dismissals.append(self.dismisser)
    // Return the link with whatever modification you need
    return link
        .isDetailLink(false)
}
}

The ViewRouterможет быть все, что вам нужно. я использовал ObservableObjectс намерением в конечном итоге добавить некоторые Publishedзначения для более сложных манипуляций со стеком в будущем:

класс ViewRouter: ObservableObject {

      var root: Binding<Bool>?
typealias Dismiss = () -> Void
var dismissals : [Dismiss] = []

func popToRoot() {
    dismissals.forEach { dismiss in
        dismiss()
    }
    dismissals = []
    root?.wrappedValue = false
}

}

Я пока не нашел решения в SwiftUI, но нашел эту библиотеку: https://github.com/knoggl/CleanUI

Используя класс CUNavigation , я могу получить именно тот шаблон навигации, который мне нужен.

Пример из readme библиотеки:

      NavigationView {
    Button(action: {
      CUNavigation.pushToSwiftUiView(YOUR_VIEW_HERE)
    }){
      Text("Push To SwiftUI View")
    }
    
    Button(action: {
      CUNavigation.popToRootView()
    }){
      Text("Pop to the Root View")
    }
    
    Button(action: {
      CUNavigation.pushBottomSheet(YOUR_VIEW_HERE)
    }){
      Text("Push to a Botton-Sheet")
    }
}

Ответ от user259521 действительно выручил, но в моей ситуации мне нужна была функциональность при нажатии каждой кнопки перед навигацией. Если вы находитесь в той же лодке, попробуйте этот код!

      //  ContentView.swift
//  Navigation View Buttons
//
//  Created by Jarren Campos on 9/10/22.
//

import SwiftUI

struct ContentView: View {

    var body: some View{
        VStack{
            ContentView1()
        }
    }
}

struct ContentView1: View {
    @State var isActive : Bool = false

    var body: some View {
        NavigationView {
            VStack{
                Button {
                    isActive = true
                } label: {
                    Text("To 2")
                }
            }
            .background{
                NavigationLink(
                    destination: ContentView2(rootIsActive: self.$isActive),
                    isActive: self.$isActive) {}
                    .isDetailLink(false)
            }
            .navigationBarTitle("One")
        }
    }
}

struct ContentView2: View {
    @Binding var rootIsActive : Bool
    @State var toThirdView: Bool = false

    var body: some View {

        VStack{
            Button {
                toThirdView = true
            } label: {
                Text("to 3")
            }
        }
        .background{
            NavigationLink(isActive: $toThirdView) {
                ContentView3(shouldPopToRootView: self.$rootIsActive)
            } label: {}
                .isDetailLink(false)
        }
        .navigationBarTitle("Two")

    }
}

struct ContentView3: View {
    @Binding var shouldPopToRootView : Bool

    var body: some View {
        VStack {
            Text("Hello, World #3!")
            Button {
                self.shouldPopToRootView = false
            } label: {
                Text("Pop to root")
            }
        }
        .navigationBarTitle("Three")
    }
}

Подробности

  • Версия Xcode 13.2.1 (13C100), Swift 5.5

Решение

Связанный список

https://github.com/raywenderlich/swift-algorithm-club/blob/master/Linked%20List/LinkedList.swift

Стек навигации

      import SwiftUI
import Combine

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// MARK: Custom NavigationLink
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

final class CustomNavigationLinkViewModel<CustomViewID>: ObservableObject where CustomViewID: Equatable {
  private weak var navigationStack: NavigationStack<CustomViewID>?
  /// `viewId` is used to find a `CustomNavigationLinkViewModel` in the `NavigationStack`
  let viewId = UUID().uuidString
  
  /// `customId` is used to mark a `CustomNavigationLink` in the `NavigationStack`. This is kind of external id.
  /// In `NavigationStack` we always prefer to use `viewId`. But from time to time we need to implement `pop several views`
  /// and that is the purpose of the `customId`
  /// Developer can just create a link with `customId` e.g. `navigationStack.navigationLink(customId: "123") { .. }`
  /// And to pop directly to  view `"123"` should use `navigationStack.popToLast(customId: "123")`
  let customId: CustomViewID?

  @Published var isActive = false {
    didSet { navigationStack?.updated(linkViewModel: self) }
  }

  init (navigationStack: NavigationStack<CustomViewID>, customId: CustomViewID? = nil) {
    self.navigationStack = navigationStack
    self.customId = customId
  }
}

extension CustomNavigationLinkViewModel: Equatable {
  static func == (lhs: CustomNavigationLinkViewModel, rhs: CustomNavigationLinkViewModel) -> Bool {
    lhs.viewId == rhs.viewId && lhs.customId == rhs.customId
  }
}

struct CustomNavigationLink<Label, Destination, CustomViewID>: View where Label: View, Destination: View, CustomViewID: Equatable {

  /// Link `ViewModel` where all states are stored
  @StateObject var viewModel: CustomNavigationLinkViewModel<CustomViewID>

  let destination: () -> Destination
  let label: () -> Label

  var body: some View {
    NavigationLink(isActive: $viewModel.isActive, destination: destination, label: label)
  }
}

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// MARK: NavigationStack
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

class NavigationStack<CustomViewID>: ObservableObject where CustomViewID: Equatable {
  
  typealias Link = WeakReference<CustomNavigationLinkViewModel<CustomViewID>>
  private var linkedList = LinkedList<Link>()

  func navigationLink<Label, Destination>(customId: CustomViewID? = nil,
                                          @ViewBuilder destination: @escaping () -> Destination,
                                          @ViewBuilder label: @escaping () -> Label)
  -> some View where Label: View, Destination: View {
    createNavigationLink(customId: customId, destination: destination, label: label)
  }
  
  private func createNavigationLink<Label, Destination>(customId: CustomViewID? = nil,
                                                        @ViewBuilder destination: @escaping () -> Destination,
                                                        @ViewBuilder label: @escaping () -> Label)
  -> CustomNavigationLink<Label, Destination, CustomViewID> where Label: View, Destination: View {
    .init(viewModel: CustomNavigationLinkViewModel(navigationStack: self, customId: customId),
          destination: destination,
          label: label)
  }
}

// MARK: Nested Types

extension NavigationStack {
  /// To avoid retain cycle it is important to store weak reference to the `CustomNavigationLinkViewModel`
  final class WeakReference<T> where T: AnyObject {
    private(set) weak var weakReference: T?
    init(value: T) { self.weakReference = value }
    deinit { print("deinited WeakReference") }
  }
}

// MARK: Searching

extension NavigationStack {
  private func last(where condition: (Link) -> Bool) -> LinkedList<Link>.Node? {
    var node = linkedList.last
    while(node != nil) {
      if let node = node, condition(node.value) {
        return node
      }
      node = node?.previous
    }
    return nil
  }
}

// MARK: Binding

extension NavigationStack {
  fileprivate func updated(linkViewModel: CustomNavigationLinkViewModel<CustomViewID>) {
    guard linkViewModel.isActive else {
      switch linkedList.head?.value.weakReference {
      case nil: break
      case linkViewModel: linkedList.removeAll()
      default:
        last (where: { $0.weakReference === linkViewModel })?.previous?.next = nil
      }
      return
    }
    linkedList.append(WeakReference(value: linkViewModel))
  }
}

// MARK: pop functionality

extension NavigationStack {
  func popToRoot() {
    linkedList.head?.value.weakReference?.isActive = false
  }
  
  func pop() {
    linkedList.last?.value.weakReference?.isActive = false
  }
  
  func popToLast(customId: CustomViewID) {
    last (where: { $0.weakReference?.customId == customId })?.value.weakReference?.isActive = false
  }
}

#if DEBUG

extension NavigationStack {
  var isEmpty: Bool { linkedList.isEmpty }
  var count: Int { linkedList.count }
  func testCreateNavigationLink<Label, Destination>(viewModel: CustomNavigationLinkViewModel<CustomViewID>,
                                                    @ViewBuilder destination: @escaping () -> Destination,
                                                    @ViewBuilder label: @escaping () -> Label)
  -> CustomNavigationLink<Label, Destination, CustomViewID> where Label: View, Destination: View {
    .init(viewModel: viewModel, destination: destination, label: label)
  }
  
}
#endif

Использование (краткий пример)

Создать навигационную ссылку:

      struct Page: View {
    @EnvironmentObject var navigationStack: NavigationStack<String>
    var body: some View {
        navigationStack.navigationLink {
            NextView(...)
        } label: {
            Text("Next page")
        }
    }
}

Поп-функциональность

      struct Page: View {
    @EnvironmentObject var navigationStack: NavigationStack<String>
    var body: some View {
        Button("Pop") {
            navigationStack.pop()
        }
        Button("Pop to Page 1") {
            navigationStack.popToLast(customId: "1")
        }
        Button("Pop to root") {
            navigationStack.popToRoot()
        }
    }
}

Использование (полный пример)

      import SwiftUI

struct ContentView: View {
  var body: some View {
    TabView {
      addTab(title: "Tab 1", systemImageName: "house")
      addTab(title: "Tab 2", systemImageName: "bookmark")
    }
  }
  
  func addTab(title: String, systemImageName: String) -> some View {
    NavigationView {
      RootPage(title: "\(title) home")
        .navigationBarTitle(title)
    }
    .environmentObject(NavigationStack<String>())
    .navigationViewStyle(StackNavigationViewStyle())
    .tabItem {
      Image(systemName: systemImageName)
      Text(title)
    }
  }
}

struct RootPage: View {
  let title: String
  var body: some View {
    SimplePage(title: title, pageCount: 0)
  }
}

struct SimplePage: View {
  @EnvironmentObject var navigationStack: NavigationStack<String>

  var title: String
  var pageCount: Int
  var body: some View {
    VStack {
      navigationStack.navigationLink(customId: "\(pageCount)") {
     // router.navigationLink {
        SimplePage(title: "Page: \(pageCount + 1)", pageCount: pageCount + 1)
      } label: {
        Text("Next page")
      }
      Button("Pop") {
        navigationStack.pop()
      }
      Button("Pop to Page 1") {
        navigationStack.popToLast(customId: "1")
      }
      Button("Pop to root") {
        navigationStack.popToRoot()
      }
    }
    .navigationTitle(title)
  }
}

Некоторые модульные тесты

      @testable import SwiftUIPop
import XCTest
import SwiftUI
import Combine

class SwiftUIPopTests: XCTestCase {
  typealias CustomLinkID = String
  typealias Stack = NavigationStack<CustomLinkID>
  private let stack = Stack()
}

// MARK: Empty Navigation Stack

extension SwiftUIPopTests {
  func testNoCrashOnPopToRootOnEmptyStack() {
    XCTAssertTrue(stack.isEmpty)
    stack.popToRoot()
  }
  
  func testNoCrashOnPopToLastOnEmptyStack() {
    XCTAssertTrue(stack.isEmpty)
    stack.popToLast(customId: "123")
  }
  
  func testNoCrashOnPopOnEmptyStack() {
    XCTAssertTrue(stack.isEmpty)
    stack.pop()
  }
}

// MARK: expectation functions

private extension SwiftUIPopTests {
  func navigationStackShould(beEmpty: Bool) {
    if beEmpty {
      XCTAssertTrue(stack.isEmpty, "Navigation Stack should be empty")
    } else {
      XCTAssertFalse(stack.isEmpty, "Navigation Stack should not be empty")
    }
  }
}

// MARK: Data / model generators

private extension SwiftUIPopTests {
  func createNavigationLink(viewModel: CustomNavigationLinkViewModel<CustomLinkID>, stack: Stack)
  -> CustomNavigationLink<EmptyView, EmptyView, CustomLinkID> {
    stack.testCreateNavigationLink(viewModel: viewModel) {
      EmptyView()
    } label: {
      EmptyView()
    }
  }
  
  func createNavigationLinkViewModel(customId: CustomLinkID? = nil) -> CustomNavigationLinkViewModel<CustomLinkID> {
    .init(navigationStack: stack, customId: customId)
  }
}

// MARK: test `isActive` changing from `true` to `false` on `pop`

extension SwiftUIPopTests {
  private func isActiveChangeOnPop(customId: String? = nil,
                                   popAction: (Stack) -> Void,
                                   file: StaticString = #file,
                                   line: UInt = #line) {
    navigationStackShould(beEmpty: true)
    let expec = expectation(description: "Wait for viewModel.isActive changing")
    
    var canalables = Set<AnyCancellable>()
    let viewModel = createNavigationLinkViewModel(customId: customId)
    let navigationLink = createNavigationLink(viewModel: viewModel, stack: stack)
    navigationLink.viewModel.isActive = true
    navigationLink.viewModel.$isActive.dropFirst().sink { value in
      expec.fulfill()
    }.store(in: &canalables)
    
    navigationStackShould(beEmpty: false)
    popAction(stack)
    waitForExpectations(timeout: 2)
    navigationStackShould(beEmpty: true)
  }
  
  func testIsActiveChangeOnPop() {
    isActiveChangeOnPop { $0.pop() }
  }
  
  func testIsActiveChangeOnPopToRoot() {
    isActiveChangeOnPop { $0.popToRoot() }
  }
  
  func testIsActiveChangeOnPopToLast() {
    let customId = "1234"
    isActiveChangeOnPop(customId: customId) { $0.popToLast(customId: customId) }
  }
  
  func testIsActiveChangeOnPopToLast2() {
    navigationStackShould(beEmpty: true)
    let expec = expectation(description: "Wait")

    var canalables = Set<AnyCancellable>()
    let viewModel = createNavigationLinkViewModel(customId: "123")
    let navigationLink = createNavigationLink(viewModel: viewModel, stack: stack)
    navigationLink.viewModel.isActive = true
    navigationLink.viewModel.$isActive.dropFirst().sink { value in
      expec.fulfill()
    }.store(in: &canalables)

    navigationStackShould(beEmpty: false)
    stack.popToLast(customId: "1234")
    DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) {
      expec.fulfill()
    }
    waitForExpectations(timeout: 3)
    navigationStackShould(beEmpty: false)
  }
}

// MARK: Check that changing `CustomNavigationLinkViewModel.isActive` will update `Navigation Stack`

extension SwiftUIPopTests {

  // Add and remove view to the empty stack
  private func isActiveChangeUpdatesNavigationStack1(createLink: (Stack) -> CustomNavigationLink<EmptyView, EmptyView, String>) {
    navigationStackShould(beEmpty: true)
    let navigationLink = createLink(stack)
    navigationStackShould(beEmpty: true)
    navigationLink.viewModel.isActive = true
    navigationStackShould(beEmpty: false)
    navigationLink.viewModel.isActive = false
    navigationStackShould(beEmpty: true)
  }

  func testIsActiveChangeUpdatesNavigationStack1() {
    isActiveChangeUpdatesNavigationStack1 { stack in
      let viewModel = createNavigationLinkViewModel()
      return createNavigationLink(viewModel: viewModel, stack: stack)
    }
  }

  func testIsActiveChangeUpdatesNavigationStack2() {
    isActiveChangeUpdatesNavigationStack1 { stack in
      let viewModel = createNavigationLinkViewModel(customId: "123")
      return createNavigationLink(viewModel: viewModel, stack: stack)
    }
  }

  // Add and remove view to the non-empty stack
  private func isActiveChangeUpdatesNavigationStack2(createLink: (Stack) -> CustomNavigationLink<EmptyView, EmptyView, String>) {
    navigationStackShould(beEmpty: true)
    let viewModel1 = createNavigationLinkViewModel()
    let navigationLink1 = createNavigationLink(viewModel: viewModel1, stack: stack)
    navigationLink1.viewModel.isActive = true
    navigationStackShould(beEmpty: false)
    XCTAssertEqual(stack.count, 1, "Navigation Stack Should contains only one link")

    let navigationLink2 = createLink(stack)
    navigationLink2.viewModel.isActive = true
    navigationStackShould(beEmpty: false)
    navigationLink2.viewModel.isActive = false
    XCTAssertEqual(stack.count, 1, "Navigation Stack Should contains only one link")
  }

  func testIsActiveChangeUpdatesNavigationStack3() {
    isActiveChangeUpdatesNavigationStack2 { stack in
      let viewModel = createNavigationLinkViewModel()
      return createNavigationLink(viewModel: viewModel, stack: stack)
    }
  }

  func testIsActiveChangeUpdatesNavigationStack4() {
    isActiveChangeUpdatesNavigationStack2 { stack in
      let viewModel = createNavigationLinkViewModel(customId: "123")
      return createNavigationLink(viewModel: viewModel, stack: stack)
    }
  }
}

Я придумал другую технику, которая работает, но она все еще кажется странной. Это также все еще оживляет отклонение обоих экранов, но это немного чище. Вы можете либо A) передать закрытие на последующие подробные экраны, либо B) передать подробности B presentationMode детали. Оба из них требуют удаления детали B, а затем задерживают на короткое время, чтобы деталь A снова появлялась на экране, прежде чем пытаться отклонить детали A.

let minDelay = TimeInterval(0.001)

struct ContentView: View {
    var body: some View {
        NavigationView {
            VStack {
                NavigationLink("Push Detail A", destination: DetailViewA())
            }.navigationBarTitle("Root View")
        }
    }
}

struct DetailViewA: View {
    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>

    var body: some View {
        VStack {
            Spacer()

            NavigationLink("Push Detail With Closure",
                           destination: DetailViewWithClosure(dismissParent: { self.dismiss() }))

            Spacer()

            NavigationLink("Push Detail with Parent Binding",
                           destination: DetailViewWithParentBinding(parentPresentationMode: self.presentationMode))

            Spacer()

        }.navigationBarTitle("Detail A")
    }

    func dismiss() {
        print ("Detail View A dismissing self.")
        presentationMode.wrappedValue.dismiss()
    }
}

struct DetailViewWithClosure: View {
    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>

    @State var dismissParent: () -> Void

    var body: some View {
        VStack {
            Button("Pop Both Details") { self.popParent() }
        }.navigationBarTitle("Detail With Closure")
    }

    func popParent() {
        presentationMode.wrappedValue.dismiss()
        DispatchQueue.main.asyncAfter(deadline: .now() + minDelay) { self.dismissParent() }
    }
}

struct DetailViewWithParentBinding: View {
    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>

    @Binding var parentPresentationMode: PresentationMode

    var body: some View {
        VStack {
            Button("Pop Both Details") { self.popParent() }
        }.navigationBarTitle("Detail With Binding")
    }

    func popParent() {
        presentationMode.wrappedValue.dismiss()
        DispatchQueue.main.asyncAfter(deadline: .now() + minDelay) { self.parentPresentationMode.dismiss() }
    }
}

Чем больше я думаю о том, как работает SwiftUI и как все устроено, тем меньше я думаю, что Apple предоставит что-то эквивалентное popToRootViewController или другие прямые изменения в стеке навигации. Он бросает вызов тому, как SwiftUI создает структуры представлений, потому что он позволяет дочернему представлению достигать состояния родителя и манипулировать им. Именно это и делают эти подходы, но они делают это явно и открыто. DetailViewA не может создать ни одно из представлений назначения, не предоставив доступ к своему собственному состоянию, то есть автору необходимо продумать последствия предоставления указанного доступа.

Вот общий подход к сложной навигации, который сочетает в себе многие подходы, описанные здесь. Этот шаблон полезен, если у вас много потоков, которые нужно возвращать в корень, а не только один.

Сначала настройте среду ObservableObject и для удобства чтения используйте перечисление для ввода ваших представлений.

class ActiveView : ObservableObject {
  @Published var selection: AppView? = nil
}

enum AppView : Comparable {
  case Main, Screen_11, Screen_12, Screen_21, Screen_22
}

[...]
let activeView = ActiveView()
window.rootViewController = UIHostingController(rootView: contentView.environmentObject(activeView))

В основном ContentView используйте кнопки с NavigationLink на EmptyView(). Мы делаем это, чтобы использовать параметр isActive в NavigationLink вместо тега и выделения. Screen_11 в основном представлении должен оставаться активным на Screen_12, и, наоборот, Screen_21 должен оставаться активным с Screen_22, иначе представления будут всплывать. Не забудьте установить для isDetailLink значение false.

struct ContentView: View {
  @EnvironmentObject private var activeView: ActiveView

  var body: some View {
    NavigationView {
      VStack {
    
        // These buttons navigate by setting the environment variable. 
        Button(action: { self.activeView.selection = AppView.Screen_1.1}) {
            Text("Navigate to Screen 1.1")
        }

        Button(action: { self.activeView.selection = AppView.Screen_2.1}) {
            Text("Navigate to Screen 2.1")
        }

       // These are the navigation link bound to empty views so invisible
        NavigationLink(
          destination: Screen_11(),
          isActive: orBinding(b: self.$activeView.selection, value1: AppView.Screen_11, value2: AppView.Screen_12)) {
            EmptyView()
        }.isDetailLink(false)

        NavigationLink(
          destination: Screen_21(),
          isActive: orBinding(b: self.$activeView.selection, value1: AppView.Screen_21, value2: AppView.Screen_22)) {
            EmptyView()
        }.isDetailLink(false)
      }
    }
  }

Вы можете использовать тот же шаблон на Screen_11, чтобы перейти к Screen_12.

Теперь прорыв в этой сложной навигации - orBinding. Это позволяет стеку представлений в потоке навигации оставаться активным. Независимо от того, находитесь ли вы на Screen_11 или Screen_12, вам необходимо, чтобы NavigationLink(Screen_11) оставался активным.

// This function create a new Binding<Bool> compatible with NavigationLink.isActive
func orBinding<T:Comparable>(b: Binding<T?>, value1: T, value2: T) -> Binding<Bool> {
  return Binding<Bool>(
      get: {
          return (b.wrappedValue == value1) || (b.wrappedValue == value2)
      },
      set: { newValue in  } // don't care the set
    )
}

Элементарно. Достаточно в корневом представлении (куда вы хотите вернуться) использовать NavigationLink с конструктором isActive. В последнем представлении переключитесь на переменную FALSE, управляющую параметром isActive.

В Swift версии 5.5 использование .isDetaillink(false) необязательно.

Вы можете использовать какой-нибудь общий класс, как в примере, или передать эту переменную вниз по иерархии VIEW через привязку. Используйте так, как вам удобнее.

      class ViewModel: ObservableObject {
    @Published var isActivate = false
}

@main
struct TestPopToRootApp: App {
    let vm = ViewModel()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(vm)
        }
    }
}

struct ContentView: View {
    @EnvironmentObject var vm: ViewModel
    
    var body: some View {
        NavigationView {
            NavigationLink("Go to view2", destination: NavView2(), isActive: $vm.isActivate)
            .navigationTitle(Text("Root view"))
        }
    }
}

struct NavView2: View {
    var body: some View {
        NavigationLink("Go to view3", destination: NavView3())
        .navigationTitle(Text("view2"))
    }
}

struct NavView3: View {
    @EnvironmentObject var vm: ViewModel
    
    var body: some View {
        Button {
            vm.isActivate = false
        } label: {
            Text("Back to root")
        }

        .navigationTitle(Text("view3"))
    }
}

Я пробовал использовать метод малхала. Но в моем случае на одной странице 2 ссылки. Вот почему я должен определить 2 переменные типа bool. Это было очень запутанно. После некоторого исследования я нашел эту ссылку на YouTube. Я использовал показанный там метод. Итак, я использовал теги в NavigationLink, и они очень полезны.

Для меня это сработало

        guard let firstScene = UIApplication.shared.connectedScenes.first as? UIWindowScene else {
                    return
                }
                
                guard let firstWindow = firstScene.windows.first else {
                    return
                }
                firstWindow.rootViewController = UIHostingController(rootView: YourSwiftUIRootView())
                firstWindow.makeKeyAndVisible()

Я создал решение, которое «просто работает». Очень доволен этим. Чтобы использовать мои волшебные решения, вам нужно сделать всего несколько шагов.

Он начинается с использования rootPresentationMode, который используется в другом месте в этом потоке. Добавьте этот код:

      // Create a custom environment key
struct RootPresentationModeKey: EnvironmentKey {
    static let defaultValue: Binding<RootPresentationMode> = .constant(RootPresentationMode())
}

extension EnvironmentValues {
    var rootPresentationMode: Binding<RootPresentationMode> {
        get { self[RootPresentationModeKey.self] }
        set { self[RootPresentationModeKey.self] = newValue }
    }
}

typealias RootPresentationMode = Bool

extension RootPresentationMode: Equatable {
    mutating func dismiss() {
        toggle()
    }
}

Далее идет магия. Он имеет два шага.

  1. Создайте модификатор представления, который отслеживает изменения в rootPresentationModeпеременная.
      struct WithRoot: ViewModifier {
    @Environment(\.rootPresentationMode) private var rootPresentationMode
    @Binding var rootBinding: Bool

    func body(content: Content) -> some View {
        content
            .onChange(of: rootBinding) { newValue in
                // we only care if it's set to true
                if newValue {
                    rootPresentationMode.wrappedValue = true
                }
            }
            .onChange(of: rootPresentationMode.wrappedValue) { newValue in
                // we only care if it's set to false
                if !newValue {
                    rootBinding = false
                }
            }
    }
}

extension View {
    func withRoot(rootBinding: Binding<Bool>) -> some View {
        modifier(WithRoot(rootBinding: rootBinding))
    }
}
  1. Добавить isPresentedко всем навигационным представлениям
      struct ContentView: View {
    // this seems.. unimportant, but it's crucial. This variable
    // lets us pop back to the root view from anywhere by adding
    // a withRoot() modifier
    // It's only used indirectly by the withRoot() modifier.
    @State private var isPresented = false

    var body: some View {
        NavigationView {
            MyMoneyMakingApp()
        }
        // rootPresentationMode MUST be set on a NavigationView to be 
        // accessible from everywhere
        .environment(\.rootPresentationMode, $isPresented)
    }

Чтобы использовать его в (любых) подпредставлениях, все, что вам нужно сделать, это

      struct MyMoneyMakingApp: View {
    @State private var isActive = false

    var body: some View {
        VStack {
            NavigationLink(destination: ADeepDeepLink(), isActive: $isActive) {
                Text("go deep")
            }
        }
        .withRoot(rootBinding: $isActive)
    }
}

struct ADeepDeepLink: View {
    @Environment(\.rootPresentationMode) private var rootPresentationMode

    var body: some View {
        VStack {
            NavigationLink(destination: ADeepDeepLink()) {
                Text("go deeper")
            }
            Button(action: {
                rootPresentationMode.wrappedValue.dismiss()
            }) {
                Text("pop to root")
            }
        }
    }
}
Другие вопросы по тегам