如何避免NavigationSplitView切换侧边栏时重建视图与重置导航栈
我按照苹果官方的导航结构示例实现了一个SwiftUI应用,使用NavigationSplitView展示分类列表和对应的食谱网格。但切换侧边栏分类后再切回原分类时,详情视图会回到根视图,且onAppear触发,说明视图被重建了。我想实现类似Apple News的行为——切换分类时保留每个分类的视图状态(包括导航栈位置、滚动位置等)。
我的核心代码如下:
主视图代码:
NavigationSplitView( columnVisibility: $navigationModel.columnVisibility ) { List( categories, selection: $navigationModel.selectedCategory ) { category in NavigationLink(category.localizedName, value: category) } .navigationTitle("Categories") .toolbar { ExperienceButton(isActive: $showExperiencePicker) } } detail: { NavigationStack(path: $navigationModel.recipePath) { RecipeGrid(category: navigationModel.selectedCategory) } }
RecipeGrid代码:
struct RecipeGrid: View { var category: Category? var dataModel = DataModel.shared var body: some View { ZStack { if let category = category { ScrollView { LazyVGrid(columns: columns) { ForEach(dataModel.recipes(in: category)) { recipe in NavigationLink(value: recipe) { RecipeTile(recipe: recipe) } .buttonStyle(.plain) } } .padding() } .navigationTitle(category.localizedName) .navigationDestination(for: Recipe.self) { recipe in RecipeDetail(recipe: recipe) { relatedRecipe in NavigationLink(value: relatedRecipe) { RecipeTile(recipe: relatedRecipe) } .buttonStyle(.plain) } } } else { Text("Choose a category") .navigationTitle("") } } } var columns: [GridItem] { [ GridItem(.adaptive(minimum: 240)) ] } }
问题出在哪
你现在的实现里,所有分类共用同一个recipePath导航路径,而且每次切换分类时,RecipeGrid会因为传入的category参数变化被重新创建,之前的导航位置、滚动状态自然就丢了。Apple News这类应用是给每个分类单独存一份状态的,这样切换的时候就能直接恢复之前的样子。
解决方法
1. 给每个分类单独存导航路径
首先修改你的NavigationModel,用字典来保存每个分类对应的导航栈路径:
@Observable class NavigationModel { var columnVisibility: NavigationSplitViewVisibility = .doubleColumn var selectedCategory: Category? // 字典存储每个分类的导航路径 var categoryRecipePaths: [Category: [Recipe]] = [:] // 动态获取当前选中分类的路径(没有的话就初始化空数组) var currentRecipePath: Binding<[Recipe]> { Binding( get: { self.categoryRecipePaths[self.selectedCategory ?? .init()] ?? [] }, set: { guard let category = self.selectedCategory else { return } self.categoryRecipePaths[category] = $0 } ) } }
2. 绑定当前分类的独立路径
修改主视图里的NavigationStack,把路径绑定到当前分类对应的独立路径上:
NavigationSplitView( columnVisibility: $navigationModel.columnVisibility ) { // 侧边栏代码不变 List( categories, selection: $navigationModel.selectedCategory ) { category in NavigationLink(category.localizedName, value: category) } .navigationTitle("Categories") .toolbar { ExperienceButton(isActive: $showExperiencePicker) } } detail: { // 绑定到当前分类的专属路径 NavigationStack(path: navigationModel.currentRecipePath) { RecipeGrid(category: navigationModel.selectedCategory) } }
3. 保留滚动位置
要让滚动位置也保留,给RecipeGrid里的ScrollView加上基于分类ID的标识,让SwiftUI把不同分类的滚动视图当成独立实例:
struct RecipeGrid: View { var category: Category? var dataModel = DataModel.shared // 可选:如果需要精确控制滚动位置,可以单独存每个分类的滚动位置 @State private var scrollPosition: CGFloat = 0 var body: some View { ZStack { if let category = category { ScrollView { LazyVGrid(columns: columns) { ForEach(dataModel.recipes(in: category)) { recipe in NavigationLink(value: recipe) { RecipeTile(recipe: recipe) } .buttonStyle(.plain) } } .padding() } .id(category.id) // 关键:用分类ID标记视图,保留独立状态 .navigationTitle(category.localizedName) .navigationDestination(for: Recipe.self) { recipe in RecipeDetail(recipe: recipe) { relatedRecipe in NavigationLink(value: relatedRecipe) { RecipeTile(recipe: relatedRecipe) } .buttonStyle(.plain) } } // 可选:绑定滚动位置,后续可以扩展成按分类存储 .scrollPosition($scrollPosition) } else { Text("Choose a category") .navigationTitle("") } } } var columns: [GridItem] { [ GridItem(.adaptive(minimum: 240)) ] } }
为啥这么管用
- 每个分类有自己的导航路径,切换时不会互相干扰,之前打开的食谱页面状态会被保留。
- 用
category.id作为ScrollView的ID,SwiftUI会识别出这是不同的视图实例,不会销毁原有状态,滚动位置自然就保留住了。 - 动态绑定的
currentRecipePath确保当前分类的导航栈和视图正确关联。
内容的提问来源于stack exchange,提问作者Basel
相关产品推荐
相关产品推荐

