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

Picker切换时如何保留DetailView中选中的内容

我用NavigationSplitView配合Picker在主视图展示两个列表,选中列表项时DetailView能正常更新,但切换Picker选项时,DetailView经常会变成无内容状态。需要实现在用户选中新内容前,始终保留当前DetailView的内容。

复现代码

import SwiftUI

struct Model1: Identifiable, Hashable {
    let id = UUID()
    let title: String
    let description: String
}

struct Model2: Identifiable, Hashable {
    let id = UUID()
    let name: String
    let address: String
}

struct CustomData: Identifiable {
    var id =  UUID()
    var docs: [Model1]
    var lites: [Model2]
    
    init(docs: [Model1], lites: [Model2]) {
        self.docs = docs
        self.lites = lites
    }
}

@MainActor
final class TestViewModel: ObservableObject {
    
    @Published var data: CustomData?
    
    func loadData() {
        let docs = [Model1(title: "title1", description: "desc1"), Model1(title: "title2", description: "desc2"), Model1(title: "title3", description: "desc3")]
        let lites = [Model2(name: "value1", address: "desc1"), Model2(name: "value2", address: "desc2"), Model2(name: "value3", address: "desc3")]
        self.data = CustomData(docs: docs, lites: lites)
    }
    
    func lites() -> [Model2] {
        return data?.lites ?? []
    }
    
    func docs() -> [Model1] {
        return data?.docs ?? []
    }
}

struct TestView2: View {
    @State private var subpage = 0
    @ObservedObject var viewModel =  TestViewModel()
    
    var body: some View {
        NavigationSplitView {
            VStack {
                Picker("Select option", selection: $subpage) {
                    Text("Lites").tag(0)
                    Text("Docs").tag(1)
                }
                .pickerStyle(.segmented)
                
                VStack() {
                    switch(subpage){
                    case 0:
                        List(viewModel.lites()) { lite in
                            NavigationLink(value: lite) {
                                HStack (alignment: .top) {
                                    Text(lite.name)
                                        .padding(.top, 10)
                                        .padding(.leading, 5)
                                        .font(.system(.headline))
                                }
                            }
                        }
                        .navigationDestination(for: Model2.self) { lite in
                            DetailView2(path: lite.name)
                        }
                    case 1:
                        List(viewModel.docs()) { doc in
                            NavigationLink(value: doc) {
                                HStack (alignment: .top) {
                                    Text(doc.title)
                                        .padding(.top, 10)
                                        .padding(.leading, 5)
                                        .font(.system(.subheadline))
                                }
                            }
                        }
                        .navigationDestination(for: Model1.self) { doc in
                            DetailView2(path: doc.title)
                        }
                    default:
                        Text("No docs")
                    }
                }
                .accentColor(.blue)
                .padding(.top, 10)
                .scrollContentBackground(.hidden)
                .background(.gray.opacity(0.1))
                
            }
        }
    detail: {
        DetailView2(path: "Example Text")
    }
    .onAppear(){
        viewModel.loadData()
    }
        
    }
}

struct DetailView2: View {
    
    var path: String
    
    var body: some View {
        VStack(spacing: .infinity) {
            Text(path)
        }
        .navigationBarHidden(true)
    }
}

struct TestView2_Previews: PreviewProvider {
    static var previews: some View {
        TestView2()
            .previewInterfaceOrientation(.landscapeRight)
            .previewDevice("iPad (9th generation)")
    }
}

问题展示

DetailView问题展示


问题原因

  1. 切换Picker选项时,原列表对应的navigationDestination会被从视图树中移除,导致之前选中的内容失去导航目标,DetailView被重置为默认占位文本(甚至因状态不匹配出现空白)。
  2. 默认DetailView是固定静态文本,未与选中内容绑定,无法持久化保存选中状态。

解决思路

  • 在ViewModel中新增@Published属性,用枚举统一管理两种类型的选中内容。
  • 将DetailView的内容与该选中属性绑定,不再依赖navigationDestination的自动跳转逻辑。
  • 点击列表项时直接更新选中属性,确保切换Picker时选中状态不丢失。

修改后的完整代码

import SwiftUI

struct Model1: Identifiable, Hashable {
    let id = UUID()
    let title: String
    let description: String
}

struct Model2: Identifiable, Hashable {
    let id = UUID()
    let name: String
    let address: String
}

// 新增枚举,兼容两种选中类型
enum SelectedItem: Hashable {
    case model1(Model1)
    case model2(Model2)
}

struct CustomData: Identifiable {
    var id = UUID()
    var docs: [Model1]
    var lites: [Model2]
    
    init(docs: [Model1], lites: [Model2]) {
        self.docs = docs
        self.lites = lites
    }
}

@MainActor
final class TestViewModel: ObservableObject {
    @Published var data: CustomData?
    // 新增:保存当前选中的内容
    @Published var selectedItem: SelectedItem?
    
    func loadData() {
        let docs = [Model1(title: "title1", description: "desc1"), Model1(title: "title2", description: "desc2"), Model1(title: "title3", description: "desc3")]
        let lites = [Model2(name: "value1", address: "desc1"), Model2(name: "value2", address: "desc2"), Model2(name: "value3", address: "desc3")]
        self.data = CustomData(docs: docs, lites: lites)
    }
    
    func lites() -> [Model2] {
        return data?.lites ?? []
    }
    
    func docs() -> [Model1] {
        return data?.docs ?? []
    }
}

struct TestView2: View {
    @State private var subpage = 0
    @ObservedObject var viewModel = TestViewModel()
    
    var body: some View {
        NavigationSplitView {
            VStack {
                Picker("Select option", selection: $subpage) {
                    Text("Lites").tag(0)
                    Text("Docs").tag(1)
                }
                .pickerStyle(.segmented)
                
                VStack {
                    switch subpage {
                    case 0:
                        List(viewModel.lites()) { lite in
                            // 点击时直接更新选中项
                            Button(action: {
                                viewModel.selectedItem = .model2(lite)
                            }) {
                                HStack(alignment: .top) {
                                    Text(lite.name)
                                        .padding(.top, 10)
                                        .padding(.leading, 5)
                                        .font(.system(.headline))
                                }
                            }
                            // 高亮当前选中的项
                            .background(viewModel.selectedItem == .model2(lite) ? Color.accentColor.opacity(0.1) : Color.clear)
                        }
                    case 1:
                        List(viewModel.docs()) { doc in
                            Button(action: {
                                viewModel.selectedItem = .model1(doc)
                            }) {
                                HStack(alignment: .top) {
                                    Text(doc.title)
                                        .padding(.top, 10)
                                        .padding(.leading, 5)
                                        .font(.system(.subheadline))
                                }
                            }
                            .background(viewModel.selectedItem == .model1(doc) ? Color.accentColor.opacity(0.1) : Color.clear)
                        }
                    default:
                        Text("No docs")
                    }
                }
                .accentColor(.blue)
                .padding(.top, 10)
                .scrollContentBackground(.hidden)
                .background(.gray.opacity(0.1))
            }
        } detail: {
            // 根据选中项显示对应内容,无选中时保留默认文本
            Group {
                switch viewModel.selectedItem {
                case .model1(let doc):
                    DetailView2(path: doc.title)
                case .model2(let lite):
                    DetailView2(path: lite.name)
                case nil:
                    DetailView2(path: "Example Text")
                }
            }
        }
        .onAppear {
            viewModel.loadData()
        }
    }
}

struct DetailView2: View {
    var path: String
    
    var body: some View {
        VStack(spacing: .infinity) {
            Text(path)
        }
        .navigationBarHidden(true)
    }
}

struct TestView2_Previews: PreviewProvider {
    static var previews: some View {
        TestView2()
            .previewInterfaceOrientation(.landscapeRight)
            .previewDevice("iPad (9th generation)")
    }
}

关键修改点

  1. 新增SelectedItem枚举,统一管理两种类型的选中内容。
  2. ViewModel中添加@Published var selectedItem: SelectedItem?,持久化保存选中状态。
  3. 将原NavigationLink替换为Button,点击时直接更新selectedItem,同时添加选中状态高亮背景。
  4. DetailView不再依赖navigationDestination,直接根据viewModel.selectedItem显示对应内容,无选中时保留默认文本。
  5. 移除原有的navigationDestination,避免切换Picker时导航目标失效的问题。

内容的提问来源于stack exchange,提问作者Ariel Antonio Fundora

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.09 06:01:45