如何在NavigationSplitView中用SceneStorage存储NavigationLink多选状态
SwiftUI SceneStorage 绑定 List 多选状态异常复现与解决方案
我们通过代码逐步复现并说明问题场景。
基础依赖代码
struct Animal: Identifiable, Hashable { var id = UUID() var name: String } // 为让Set类型支持SceneStorage存储实现的RawRepresentable扩展 extension Set: RawRepresentable where Element: Codable { public typealias RawValue = String public var rawValue: String { guard let data = try? JSONEncoder().encode(self), let string = String(data: data, encoding: .utf8) else { return "[]" } return string } public init?(rawValue: String) { guard let data = rawValue.data(using: .utf8), let result = try? JSONDecoder().decode([Element].self, from: data) else { return nil } self = Set(result) } }
场景1:@State + 纯Text列表项,功能正常
struct ContentView: View { @State private var sampleData = [ Animal(name: "Bird"), Animal(name: "Cat"), Animal(name: "Dog"), Animal(name: "Fish") ] @State private var multiSelection = Set<UUID>() // @SceneStorage("multiSelection") // private var multiSelection = Set<UUID>() var body: some View { NavigationSplitView { List(sampleData, selection: $multiSelection) { animal in Text(animal.name) } } detail: { Text("Detail") } } }
该场景使用@State声明Set<UUID>类型的多选集合multiSelection,List内直接通过Text渲染列表项,未使用NavigationLink,多选功能运行正常。
场景2:@SceneStorage + 纯Text列表项,功能正常
struct ContentView: View { @State private var sampleData = [ Animal(name: "Bird"), Animal(name: "Cat"), Animal(name: "Dog"), Animal(name: "Fish") ] // @State // private var multiSelection = Set<UUID>() @SceneStorage("multiSelection") private var multiSelection = Set<UUID>() var body: some View { NavigationSplitView { List(sampleData, selection: $multiSelection) { animal in Text(animal.name) } } detail: { Text("Detail") } } }
该场景将@State替换为@SceneStorage("multiSelection")声明多选集合,List内仍直接通过Text渲染列表项,多选状态持久化功能运行正常。
场景3:@State + NavigationLink列表项,功能正常
struct ContentView: View { @State private var sampleData = [ Animal(name: "Bird"), Animal(name: "Cat"), Animal(name: "Dog"), Animal(name: "Fish") ] @State private var multiSelection = Set<UUID>() // @SceneStorage("multiSelection") // private var multiSelection = Set<UUID>() var body: some View { NavigationSplitView { List(sampleData, selection: $multiSelection) { animal in NavigationLink(animal.name, value: animal.id) } } detail: { Text("Detail") } } }
该场景使用@State声明多选集合,List内通过NavigationLink绑定动物id作为导航值,多选功能运行正常。
场景4:@SceneStorage + NavigationLink列表项,功能异常
struct ContentView: View { @State private var sampleData = [ Animal(name: "Bird"), Animal(name: "Cat"), Animal(name: "Dog"), Animal(name: "Fish") ] // @State // private var multiSelection = Set<UUID>() @SceneStorage("multiSelection") private var multiSelection = Set<UUID>() var body: some View { NavigationSplitView { List(sampleData, selection: $multiSelection) { animal in NavigationLink(animal.name, value: animal.id) } } detail: { Text("Detail") } } }
该场景同时使用@SceneStorage声明多选集合、List内通过NavigationLink绑定导航值时,选中列表项后会立即被取消选中,多选状态无法正常存储。
故障原因
问题出在@SceneStorage的绑定机制和NavigationSplitView的状态同步逻辑冲突:
- 直接给List传入
@SceneStorage的绑定时,每次选择状态变更都会触发Set的RawRepresentable序列化/反序列化流程,生成全新的Set实例 - NavigationLink触发导航状态更新时,会读取当前selection绑定的值,因为序列化生成的新实例和当前选中的实例内存地址不一致,List会判定选择状态被外部重置,直接清空选中项,最终表现为点击就取消选中。
修复方案
使用@State作为UI层和持久化层的中间桥接,避免@SceneStorage直接参与UI高频状态同步,代码如下:
struct ContentView: View { @State private var sampleData = [ Animal(name: "Bird"), Animal(name: "Cat"), Animal(name: "Dog"), Animal(name: "Fish") ] // 仅用于UI实时交互的选择状态,无序列化开销 @State private var multiSelection = Set<UUID>() // 仅用于持久化存储,不直接绑定UI控件 @SceneStorage("multiSelection") private var storedMultiSelection: Set<UUID> = [] var body: some View { NavigationSplitView { List(sampleData, selection: $multiSelection) { animal in NavigationLink(animal.name, value: animal.id) } .onAppear { // 页面加载时从持久化存储恢复历史选中状态 multiSelection = storedMultiSelection } .onChange(of: multiSelection) { newValue in // 选中状态变化后同步写入持久化存储 storedMultiSelection = newValue } } detail: { Text("Detail") } } }
该方案既保留了SceneStorage的场景状态恢复能力,也规避了高频UI更新时反复序列化导致的状态不一致问题,多选、导航、状态持久化功能均可正常运行。
内容的提问来源于stack exchange,提问作者Mark
相关产品推荐
相关产品推荐

