SwiftUI NavigationStack、可搜索修饰符和返回根视图的问题

问题描述 投票:0回答:1

在使用可搜索修饰符时,我遇到了 SwiftUI 的 NavigationStack 问题。在视图之间导航时,一切都按预期工作,但如果我使用搜索栏过滤列表,然后点击过滤结果,我可以导航到下一个视图。然而,在随后的视图中,我的“设置并返回根”按钮(本应调用 popToRoot())不起作用。设置如下:

结构: RootView:包含包含项目 1-7 的列表。 ActivityView:包含可以使用可搜索修饰符过滤的活动列表。 设置视图:包含一个标记为“设置并返回根”的按钮,该按钮调用 popToRoot() 导航回根视图。

根视图

struct RootView: View {
    @EnvironmentObject var navManager: NavigationStateManager
    
    var body: some View {
        NavigationStack(path: $navManager.selectionPath) {
            List(1...7, id: \.self) { item in
                Button("Element \(item)") {
                    // Navigate to ActivityView with an example string
                    navManager.selectionPath.append(NavigationTarget.activity)
                }
            }
            .navigationTitle("Root View")
            .navigationDestination(for: NavigationTarget.self) { destination in
                switch destination {
                case .activity:
                    ActivityView()
                case .settings:
                    SettingsView()
                }
            }
        }
    }
}

活动视图

struct ActivityView: View {
    @EnvironmentObject var navManager: NavigationStateManager
    
    let activities = ["Running", "Swimming", "Cycling", "Hiking", "Yoga", "Weightlifting", "Boxing"]
    
    @State private var searchText = ""
    
    var filteredActivities: [String] {
        if searchText.isEmpty {
            return activities
        } else {
            return activities.filter { $0.localizedCaseInsensitiveContains(searchText) }
        }
    }
    
    var body: some View {
        List {
            ForEach(filteredActivities, id: \.self) { activity in
                NavigationLink(
                    destination: SettingsView(), // Navigiere zur SettingsView
                    label: {
                        HStack {
                            Text(activity)
                                .padding()
                            Spacer()
                        }
                    }
                )
            }
        }
        
        .navigationTitle("Choose Activity")
        .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always), prompt: "Search Activities")
    }
}

设置视图

struct SettingsView: View {
    @EnvironmentObject var navManager: NavigationStateManager

    var body: some View {
        VStack {
            Text("Settings")
                .font(.largeTitle)
                .padding()

            Button("Set and Return to Root") {
                // Pop to the root view when the button is pressed
                navManager.popToRoot()
            }
            .padding()
            .background(Color.blue)
            .foregroundColor(.white)
            .cornerRadius(10)
        }
        .navigationTitle("Settings")
    }
}

导航状态管理器

// Define enum globally at the top
enum NavigationTarget {
    case activity
    case settings
}

class NavigationStateManager: ObservableObject {
    @Published var selectionPath = NavigationPath()

    func popToRoot() {
        selectionPath = NavigationPath()
    }

    func popView() {
        selectionPath.removeLast()
    }
}

// RootView, ActivityView, SettingsView, and ContentView follow as described earlier...

问题:

当我在 ActivityView 中搜索并点击筛选结果时,我成功导航到 SettingsView。然而,在此视图中,按下“Set and Return to Root”按钮不会触发返回 RootView 的导航,即使 popToRoot() 正在被调用。

此问题仅在使用搜索栏和过滤结果时出现。如果我不使用搜索栏进行导航,该按钮将按预期工作。

问题:

为什么 popToRoot() 函数在搜索操作后失败,如何确保过滤列表后可以返回到根视图?

任何见解或建议将不胜感激!

swiftui swiftui-navigationstack swiftui-searchable
1个回答
0
投票

问题出在您的 ActivityView 中,您“手动”导航,即不影响路径,该路径需要 NavigationTarget 的值/大小写(根据 RootView 中

.navigationDestination
的配置。

要修复此问题,请使用 NavigationTarget 的

destination
作为 NavigationLink,而不是设置
value
。所以你的
filteredActivities
循环应该是:

ForEach(filteredActivities, id: \.self) { activity in
    NavigationLink(
        value: NavigationTarget.settings, // <- Here, use value instead of destination

        label: {
            HStack {
                Text(activity)
                    .padding()
                Spacer()
            }
        }
    )
}

这是完整的代码,其中包括必要的预览设置(对于 XCode 16,使用 @Previewable),这是您的原始代码所遗漏的。

import SwiftUI

struct RootView: View {
    @EnvironmentObject var navManager: NavigationStateManager

    var body: some View {
        NavigationStack(path: $navManager.selectionPath) {
            List(1...7, id: \.self) { item in
                Button("Element \(item)") {
                    // Navigate to ActivityView with an example string
                    navManager.selectionPath.append(NavigationTarget.activity)
                }
            }
            .navigationTitle("Root View")
            .navigationDestination(for: NavigationTarget.self) { destination in
                switch destination {
                    case .activity:
                        ActivityView()
                    case .settings:
                        ActivitySettingsView()
                }
            }
        }
    }
}

struct ActivityView: View {
    @EnvironmentObject var navManager: NavigationStateManager

    let activities = ["Running", "Swimming", "Cycling", "Hiking", "Yoga", "Weightlifting", "Boxing"]
    
    @State private var searchText = ""
    
    var filteredActivities: [String] {
        if searchText.isEmpty {
            return activities
        } else {
            return activities.filter { $0.localizedCaseInsensitiveContains(searchText) }
        }
    }
    
    var body: some View {
        List {
            ForEach(filteredActivities, id: \.self) { activity in
                NavigationLink(
                    value: NavigationTarget.settings, // <- Here, use value instead of destination

                    label: {
                        HStack {
                            Text(activity)
                                .padding()
                            Spacer()
                        }
                    }
                )
            }
        }
        .navigationTitle("Choose Activity")
        .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always), prompt: "Search Activities")
    }
}

struct ActivitySettingsView: View {
    @EnvironmentObject var navManager: NavigationStateManager
    
    var body: some View {
        VStack {
            Text("Settings")
                .font(.largeTitle)
                .padding()
            
            Button("Set and Return to Root") {
                // Pop to the root view when the button is pressed
                navManager.popToRoot()
            }
            .padding()
            .background(Color.blue)
            .foregroundColor(.white)
            // .cornerRadius(10) // <- Deprecated
            .clipShape(RoundedRectangle(cornerRadius: 10)) // <- Use this instead
        }
        .navigationTitle("Settings")
    }
}

// Define enum globally at the top
enum NavigationTarget {
    case activity
    case settings
}

class NavigationStateManager: ObservableObject {
    @Published var selectionPath = NavigationPath()
    
    func popToRoot() {
        selectionPath = NavigationPath()
    }
    
    func popView() {
        selectionPath.removeLast()
    }
}

// RootView, ActivityView, SettingsView, and ContentView follow as described earlier...

#Preview {
    @Previewable @StateObject var navManager: NavigationStateManager = NavigationStateManager()
    
    RootView()
        .environmentObject(navManager)
    
}

我注意到你仍在使用旧的 Observable 对象协议,包括 ObservableObject、EnvironmentObject 等。我不知道这是否是故意的,但你应该考虑迁移到新的 Observable 宏

这是上面代码的一个版本,它使用了 Observable 宏:

import SwiftUI
import Observation

struct RootView: View {
    @Environment(NavigationStateManager.self) private var navManager: NavigationStateManager

    
    var body: some View {
        
        @Bindable var navManager = navManager //get a binding to the Environment object
        
        NavigationStack(path: $navManager.selectionPath) {
            List(1...7, id: \.self) { item in
                Button("Element \(item)") {
                    // Navigate to ActivityView with an example string
                    navManager.selectionPath.append(NavigationTarget.activity)
                }
            }
            .navigationTitle("Root View")
            .navigationDestination(for: NavigationTarget.self) { destination in
                switch destination {
                    case .activity:
                        ActivityView()
                    case .settings:
                        ActivitySettingsView()
                        
                }
            }
        }
    }
}

struct ActivityView: View {
    @Environment(NavigationStateManager.self) private var navManager: NavigationStateManager
    
    let activities = ["Running", "Swimming", "Cycling", "Hiking", "Yoga", "Weightlifting", "Boxing"]
    
    @State private var searchText = ""
    
    var filteredActivities: [String] {
        if searchText.isEmpty {
            return activities
        } else {
            return activities.filter { $0.localizedCaseInsensitiveContains(searchText) }
        }
    }
    
    var body: some View {
        List {
            ForEach(filteredActivities, id: \.self) { activity in
                NavigationLink(
                    value: NavigationTarget.settings, // <- Here, use value instead of destination
                    // destination: SettingsView(), // Navigiere zur SettingsView
                    
                    label: {
                        HStack {
                            Text(activity)
                                .padding()
                            Spacer()
                        }
                    }
                )
            }
        }
        
        .navigationTitle("Choose Activity")
        .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always), prompt: "Search Activities")
    }
}

struct ActivitySettingsView: View {
    @Environment(NavigationStateManager.self) private var navManager: NavigationStateManager
    
    var body: some View {
        VStack {
            Text("Settings")
                .font(.largeTitle)
                .padding()
            
            Button("Set and Return to Root") {
                // Pop to the root view when the button is pressed
                navManager.popToRoot()
            }
            .padding()
            .background(Color.blue)
            .foregroundColor(.white)
            // .cornerRadius(10) // <- Deprecated
            .clipShape(RoundedRectangle(cornerRadius: 10)) // <- Use this instead
        }
        .navigationTitle("Settings")
    }
}

// Define enum globally at the top
enum NavigationTarget {
    case activity
    case settings
}

@Observable
class NavigationStateManager {
    
    var selectionPath = NavigationPath()
    
    func popToRoot() {
        selectionPath = NavigationPath()
    }
    
    func popView() {
        selectionPath.removeLast()
    }
}

// RootView, ActivityView, SettingsView, and ContentView follow as described earlier...

#Preview {
    @Previewable @State var navManager: NavigationStateManager = NavigationStateManager()
    
    RootView()
        .environment(navManager)
    
}

如果您计划使用 TabView 以及可能的多个导航堆栈(每个导航堆栈都有其路径),我在here提供了一个答案,它展示了如何进行操作,它允许控制特定选项卡/路径的 popToRoot。

© www.soinside.com 2019 - 2024. All rights reserved.