popToRootViewController in SwiftUI

What is the best practice to achieve popToRootViewController like functionality in SwiftUI? There are couple of ways to do it and one of them is described nicely here (https://stackoverflow.com/questions/57334455/swiftui-how-to-pop-to-root-view) via publish event but it losses the animation while popping to root. Please suggest.

同问,有没有好的解决办法?
we used the approach with navigation links. Alternatively, you can open the flow in a sheet and close the sheet altogether.

With iOS 16, you can now use NavigationStack. You can use the NavigationPath variable in NavigationStack in order to take control over the stack of views you have pushed and popped.

NavigationPath allows you to create a type-erasing variable to keep track of your views, but it is easy to create one with any type, like below, where I use an array of Strings.

struct ContentView : View {
    @State private var path: [String]
    var body: some View {
        NavigationStack(path: $path){
            List{
                NavigationLink("title", value: "nextView")
            }
            .navigationDestination(for: String.self){ str in
                DetailView(strToShow: str)
            
            }
        } 
        
    }
   
}

Each NavigationLink will pass a value to the .navigationDestination modifier and will add the view to the path variable. Then, at any point in time, you can call path.popLast() to go backwards by one view, or set the path back to an empty array like this: path = []. This will pop you to the first page. This can be called in a matter of ways, for instance you can add a button to the toolbar that does this.

But this (like every example I can find) relies on a highly unlikely scenario: a stack of views that all take the same datatype. All the NavigationLinks go to a common NavigationDestination.

Far more common than that are applications that involve completing something step-by-step. For example, filling out an order. The root view might be the order summary. Then the user proceeds through the customer-search view, the customer-detail view, an address-selection view... and now he's done with that part of the order and needs to dismiss that entire stack and return to the root.

For that kind of situation, you don't use NavigationDestination. Your NavigationLinks invoke each view directly, passing each one whatever unique data that view needs. Nowhere (including in Apple's doc) have I seen an explanation of how NavigationPath gets populated in a realistic progression of disparate views. When do you add a path element? How is each element associated with a view?

Nobody ever says.

Thanks for the extensive response!

My situation is #2. I don't use NavigationDestination, because most of my NavigationLinks are single-use ones invoked with a button, which go to a specific view to handle a certain task.

In my application, the user is preparing a message to send to another user or contact. From the root view (where the NavigationStack is declared), the user goes through a series of screens to pick a message recipient and a communication method.

Because each child view needs access to the NavigationPath, I enclose it in an object I can pass through the hierarchy (because I think environment variables are hokey):

@Observable
class NavPathController
{
	var path: NavigationPath

	init()
	{
		path = NavigationPath()
	}

	func popOne()
	{
		path.removeLast()
	}

	func popAll()
	{
		path.removeLast(path.count)
	}
}

In my root view, I do:

@State private var viewStack = NavPathController()

var body: some View
{
	NavigationStack(path: $viewStack.path)
	{
		NavigationLink(destination: UserFindingView(withMessage: theMessage, viewPathController: viewStack), label: { Text("Pick a recipient") })
		...
	}
}

and then each child view looks something like

struct UserFindingView: View
{
	private var			theMessage: Message
	private var			pathControl: NavPathController?

	init(withMessage: Message, viewPathController: NavPathController? = nil)
	{
		theMessage = withMessage
		self.pathControl = viewPathController
	}

	var body: some View
	{
		NavigationLink(destination: ContactsStartView(withMessage: theMessage, viewPathController: pathControl), label: { Text("Use your contacts...") })
	}

But as the user follows the NavigationLinks, nothing appears to be added to the path. At the end of the hierarchy (the topmost overlaid view), the path has zero elements. So... how does one address that?

Hi @Stokestack , You can absolutely use different data types!

When do you add a path element? How is each element associated with a view?

I'll add an example at the bottom here, but when you use NavigationPath (although I think the documentation here is more helpful), you can initialize it with a NavigationPath type, which is type-erased.

There are two ways to add path elements.

  1. With the .navigationDestination modifier
  2. Appending it yourself this is a modified post...number 2 has been deleted

For #1, you can have as many of these modifiers presenting different destinations for different data types as you'd like. Your question was based on having a different view for each one-off item. You do in fact still need to have navigationDestination modifiers for each one, but there's an easier way to do this if you use an enum. I'll leave an example in the snippet below.

For #3, you can use a button to append a view instead of using a NavigationLink, like:

Button("Go to next view") {
   path.append("newView") // this would use the navigationDestination for String on the stack
}

Here is an entire example that highlights using NavigationPath. I put it in an observable class so you can reference it from any view. You can pop back as many views as you'd like, or set the variable back to .init() which will reset it.

import SwiftUI

//
//  ContentView.swift
//  NavStackPop
//
//  Created by Sydney Allison on 3/13/25.
//

import SwiftUI

@Observable
final class AppNavigationModel {
    var path: NavigationPath = .init()
}

// Have as many enums as you'd like. You then don't need a new navigationDestination for every single data type.
enum ViewType {
    case detail
    case thirdView
}

struct ContentView: View {
    @Environment(AppNavigationModel.self) private var appNavigationModel
    var body: some View {
        @Bindable var appNavigationModel = appNavigationModel
        NavigationStack(path: $appNavigationModel.path) {
            List {
                NavigationLink("Go to Detail", value: ViewType.detail)
                NavigationLink("Go to third view", value: ViewType.thirdView)
            }
            .navigationDestination(for: ViewType.self, destination: { nav in
                switch nav {
                    case .detail:
                    DetailView()
                    case .thirdView:
                    ThirdView()
                }
            })
        }
        .padding()
    }
}

struct DetailView: View {
    @Environment(AppNavigationModel.self) private var appNavigationModel
    var body: some View {
        List {
            NavigationLink("Go to another view", value: ViewType.thirdView)
        }
       
    }
}

struct ThirdView: View {
    @Environment(AppNavigationModel.self) private var appNavigationModel
    var body: some View {
        VStack {
            Text("Third View")
            Button("pop to root") {
                print(appNavigationModel.path) // This will print detail and third views
                appNavigationModel.path = .init()
            }
        }
        
    }
}

I hope this clears it up. I recommend filing an enhancement report at https://feedbackassistant.apple.com to request functionality of adding views directly to the path with the NavigationLink initializer. We really do look at these and they're really important in terms of what we update/improve. Please comment back with the FB number if you do this.

One workaround you could take if you can't follow the enum path I posted below (not necessarily best recommended but an idea :) )

You could use NavigationPath for only one view. then, when you pop to root, you'll always pop to the root view because there's only one view on the stack. For example:

struct ContentView: View {
    @Environment(AppNavigationModel.self) private var appNavigationModel
    var body: some View {
        @Bindable var appNavigationModel = appNavigationModel
        NavigationStack(path: $appNavigationModel.path) {
            List {
                NavigationLink("Go to Detail", value: "Detail")
            }
            .navigationDestination(for: String.self, destination: { nav in
                DetailView()
            })
        }
        .padding()
    }
}

In DetailView, let's say you link to another view, which links to another and so on until you're 30 views in and want to go back to ContentView. Well, right now, all that's on the path variable is DetailView, so popping back (with path = .init()) would pop you back to the root.

I thought your enum idea was a clever workaround... but then I realized that I would have to add an enum for every child view of every child view to which the user needs to navigate. A maintenance hassle, to say the least.

So yes, I will probably do something like what you suggest above.

Feedback filed: FB16858715

I ended up going with a combo: your enum idea, with an enum for the first view of each stack a user can invoke from the root view. So the path consists of one entry, tied to the bottom view of the stack I want to dismiss.

I still use my NavPathController object to pass the path through all the views, so the last child can clear the path and collapse the whole stack back to the root.

popToRootViewController in SwiftUI
 
 
Q