You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何避免NavigationSplitView切换侧边栏时重建视图与重置导航栈

SwiftUI 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.10 12:40:11