[Suggestion] SwiftUI convenience environment navigation functions

I've been thinking a lot about how navigation and presentation are managed in SwiftUI, and I wanted to propose an idea for a more streamlined approach using environment values. Right now, handling navigation can feel fragmented — especially when juggling default NavigationStack, modals, and tab selections.

What if SwiftUI provided a set of convenience environment values for these common actions?

Tabs

@Environment(\.selectedTab) var selectedTab
@Environment(\.selectTab) var selectTab
  • selectedTab: Read the current tab index
  • selectTab(index: Int): Programmatically switch tabs

Stack Navigation

@Environment(\.stackCount) var stackCount
@Environment(\.push) var push
@Environment(\.pop) var pop
@Environment(\.popToRoot) var popToRoot
  • stackCount: Read how many views are in the navigation stack
  • push(destination: View): Push a new view onto the stack
  • pop(last: Int = 1): Pop the last views
  • popToRoot(): Return to the root view

Modals

@Environment(\.sheet) var sheet
@Environment(\.fullScreenCover) var fullScreenCover
@Environment(\.popover) var popover
@Environment(\.dismissModal) var dismissModal
  • sheet(view: View): Present a sheet
  • fullScreenCover(view: View): Present a full-screen cover
  • popover(view: View): Show a popover
  • dismissModal(): Dismiss any presented modal

Alerts & Dialogs

@Environment(\.alert) var alert
@Environment(\.confirmationDialog) var confirmationDialog
@Environment(\.openAppSettings) var openAppSettings
  • alert(title: String, message: String): Show an alert
  • confirmationDialog(title: String, actions: [Button]): Show a confirmation dialog
  • openAppSettings(): Directly open the app’s settings

Why?

  1. Clean syntax: This keeps navigation code clean and centralized.
  2. Consistency: Environment values already manage other app-level concerns (color scheme, locale, etc.). Why not navigation too?
  3. Reusability: This approach is easy to adapt across different view hierarchies.

Example

@main
struct App: App {
    var body: some Scene {
        WindowGroup {
            TabView {
                NavigationStack {
                    ProductList()
                }
                .tabItem { ... }
                
                NavigationStack {
                    OrderList()
                }
                .tabItem { ... }
            }
        }
    }
}

struct ProductList: View {
    @Environment(\.push) var push
    
    @State var products: [Product] = []
    
    var body: some View {
        List(protucts) { product in
            Button {
                push(destination: ProductDetails(product: product))
                }
            } label: {
                ...
            }
        }
        .task { ... }
    }
}

struct ProductDetails: View { ... }
@Environment(\.selectedTab) var selectedTab
@Environment(\.selectTab) var selectTab

Can be just one using binding.

extension EnvironmentValues {
    @Entry public var selectedTab: Binding<Int> = .constant(0)
}

You’re welcome to file an enhancement request using Feedback Assistant. Once you file the request, please post the FB number here. If you're not familiar with how to file enhancement requests, take a look at Bug Reporting: How and Why?

I created all the features written above. You are free to use and change.

import SwiftUI

public struct EVTabView<Content: View>: View {
    let content: () -> Content
    @State private var selection: Int = 0
    
    public init(initialSelection: Int = 0, @ViewBuilder content: @escaping () -> Content) {
        self.selection = initialSelection
        self.content = content
    }
    
    public var body: some View {
        TabView(selection: $selection, content: content)
            .environment(\.selectedTab, $selection)
            .evModal()
    }
}

extension EnvironmentValues {
    @Entry public var selectedTab: Binding<Int> = .constant(0)
}
import SwiftUI

public struct EVNavigationStack<Root: View>: View {
    let root: () -> Root
    @State private var path: [EVDestination] = []
    @Environment(\.dismiss) private var dismiss
    
    public init(@ViewBuilder root: @escaping () -> Root) {
        self.root = root
    }
    
    public var body: some View {
        NavigationStack(path: $path) {
            root()
                .navigationDestination(for: EVDestination.self) {
                    AnyView($0.view)
                }
        }
        .environment(\.stackCount, path.count)
        .environment(\.push, { path.append(EVDestination(view: $0)) })
        .environment(\.pop, { if !path.isEmpty { path.removeLast() } })
        .environment(\.popToLast, { if path.count >= $0 { path.removeLast($0) } })
        .environment(\.popToRoot, { path.removeAll() })
        .environment(\.dismissStack, { dismiss() })
        .evModal()
    }
}

extension EnvironmentValues {
    @Entry public var stackCount: Int = 0
    @Entry public var push: (any View) -> Void = { _ in }
    @Entry public var pop: () -> Void = { }
    @Entry public var popToLast: (Int) -> Void = { _ in }
    @Entry public var popToRoot: () -> Void = { }
    @Entry public var dismissStack: () -> Void = { }
}
import SwiftUI

struct EVModalModifier: ViewModifier {
    @State private var sheet: EVDestination? = nil
    @State private var fullScreenCover: EVDestination? = nil
    @State private var alert: EVMessage? = nil
    @State private var confirmationDialog: EVMessage? = nil
    
    func body(content: Content) -> some View {
        content
            .sheet(item: $sheet) { AnyView($0.view) }
            .fullScreenCover(item: $fullScreenCover) { AnyView($0.view) }
            .environment(\.sheet, { sheet = EVDestination(view: $0) })
            .environment(\.fullScreenCover, { fullScreenCover = EVDestination(view: $0) })
            .environment(\.alert, { alert = $0 })
            .environment(\.confirmationDialog, { confirmationDialog = $0 })
            .confirmationDialog(confirmationDialog?.title ?? "",
                                isPresented: Binding { confirmationDialog != nil } set: { if !$0 { confirmationDialog = nil } },
                                titleVisibility: .visible) {
                if let actions = confirmationDialog?.actions {
                    AnyView(actions())
                }
            } message: {
                if let message = confirmationDialog?.message {
                    Text(message)
                }
            }
            .alert(alert?.title ?? "",
                   isPresented: Binding { alert != nil } set: { if !$0 { alert = nil } }) {
                if let actions = alert?.actions {
                    AnyView(actions())
                }
            } message: {
                if let message = alert?.message {
                    Text(message)
                }
            }
    }
}

extension EnvironmentValues {
    @Entry public var sheet: (any View) -> Void = { _ in }
    @Entry public var fullScreenCover: (any View) -> Void = { _ in }
    @Entry public var alert: (EVMessage) -> Void = { _ in }
    @Entry public var confirmationDialog: (EVMessage) -> Void = { _ in }
}

extension View {
    public func evModal() -> some View {
        self.modifier(EVModalModifier())
    }
}
import SwiftUI

public struct EVMessage {
    let title: LocalizedStringKey
    let message: LocalizedStringKey?
    let actions: () -> any View
    
    public init(title: LocalizedStringKey,
                message: LocalizedStringKey? = nil,
                @ViewBuilder actions: @escaping () -> any View) {
        self.title = title
        self.message = message
        self.actions = actions
    }
}
import SwiftUI

struct EVDestination: Identifiable, Hashable {
    let id = UUID()
    let view: any View
    
    static func == (lhs: Self, rhs: Self) -> Bool { lhs.id == rhs.id }
    func hash(into hasher: inout Hasher) { hasher.combine(id) }
}

Using example

    EVTabView {
        EVNavigationStack {
            ContentView()
        }
        .tabItem {
            Label("Menu", systemImage: "list.dash")
        }
        .tag(0)
        
        EVNavigationStack {
            ContentView()
        }
        .tabItem {
            Label("Order", systemImage: "square.and.pencil")
        }
        .tag(1)
    }
struct ContentView: View {
    @Environment(\.selectedTab) @Binding private var selectedTab
    @Environment(\.stackCount) private var stackCount
    @Environment(\.push) private var push
    @Environment(\.sheet) private var sheet
    @Environment(\.fullScreenCover) private var fullScreenCover
    @Environment(\.dismiss) private var dismiss
    @Environment(\.alert) private var alert
    @Environment(\.confirmationDialog) private var confirmationDialog
    
    var body: some View {
        VStack(spacing: 15) {
            Text("Selected tab: **\(selectedTab)**")
            Text("Stack count: **\(stackCount)**")
            
            Button("change tab") {
                if selectedTab == 1 { selectedTab = 0 }
                else { selectedTab = 1 }
            }
            
            Button("push") {
                push(DetailView())
            }
            
            Button("sheet") {
                sheet(ContentView())
            }
            
            Button("fullScreenCover") {
                fullScreenCover(EVNavigationStack { ContentView() })
            }
            
            Button("dismiss") {
                dismiss()
            }
            
            Button("alert") {
                let message = EVMessage(title: "Lorem ipsum",
                                        message: "Lorem ipsum dolor sit amet") {
                    Button("OK") { }
                }
                
                alert(message)
            }
            
            Button("confirmationDialog") {
                let message = EVMessage(title: "Lorem ipsum",
                                        message: "Lorem ipsum dolor sit amet") {
                    Button("OK") { }
                }
                
                confirmationDialog(message)
            }
        }
    }
}

FB17017002 (Convenience environment values / actions for common app navigation)

[Suggestion] SwiftUI convenience environment navigation functions
 
 
Q