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

如何在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.26 15:57:50