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

如何通过NavigationSplitView向第二列传递数据?

解决NavigationSplitView下macOS双列应用的数据传递问题

问题背景

开发macOS双列应用,左侧列表展示处理单元(文件)的文件名和日期,右侧列在选中文件时展示内容并支持编辑。使用NavigationSplitView时遇到以下问题:

  • 直接为@State变量赋值报错,不符合View协议
  • 通过selectedUnitId获取索引失败
  • 用NavigationLink跳转至UnitView后,选中新单元时TextEditor文本不更新,但已弃用的NavigationView功能正常

错误尝试代码

尝试1:直接在detail部分用TextEditor绑定本地@State

struct Unit: Codable, Hashable, Identifiable {
    let id: UUID
    var text: String
    var date = Date()
    var dateText: String {
        let df = DateFormatter()
        df.dateFormat = "EEEE, MMM d yyyy, h:mm a"
        return df.string(from: date)
    }
    var changed: Bool = false
}

final class UnitModel: ObservableObject {
    @AppStorage("unit") public var units: [Unit] = []
    init() {
        self.units = self.units.sorted(by: {
            $0.date.compare($1.date) == .orderedDescending
        })
    }
    
    func sortList() {
        self.units = self.units.sorted(by: {
            $0.date.compare($1.date) == .orderedDescending
        })
    }
}

struct ContentView: View {
    @EnvironmentObject private var data: UnitModel
    @State var selectedUnitId: UUID?
    @State var text: String = ""

    var body: some View {
        NavigationSplitView {
              List(data.units, selection: $selectedUnitId) { currentunit in
                  VStack(alignment: .leading) {
                      Text(currentunit.filename)
                      Text(currentunit.dateText)
                  }
              }
          } detail: {
              VStack(alignment: .leading) {
                  TextEditor(text: $text)
              }
          }
    }
}

尝试2:用NavigationLink跳转至UnitView

struct ContentView: View {
    @EnvironmentObject private var data: UnitModel
    @State var selectedNoteId: UUID?

    var body: some View {
        NavigationSplitView {
              List(data.units, selection: $selectedNoteId) { currentunit in
                  NavigationLink{
                      UnitView(unit: currentunit, text: currentunit.text)
                  } label: {
                      VStack(alignment: .leading) {
                          Text(currentunit.filename)
                          Text(currentunit.dateText)
                      }
                  }
              }
          } detail: {
              Text("Select a unit.")
          }
    }
}

struct UnitView: View {
    @EnvironmentObject private var data: UnitModel
    var unit: Unit
    @State var text: String
        
    var body: some View {
        VStack(alignment: .leading) {
            TextEditor(text: $text)
        }
    }
}

旧的可用(已弃用)代码

NavigationView {
    List(data.units, selection: $selectedNoteId) { currentunit in
        NavigationLink(
            destination: UnitView(unit: currentunit, text: currentunit.text),
            label: {
                VStack(alignment: .leading) {
                    Text(currentunit.filename)
                    Text(currentunit.dateText)
                }
            }
        )
    }
}

核心问题分析

  1. 本地@State变量是单向状态,无法自动响应选中单元的变化,且在View的body中直接赋值会违反纯函数要求
  2. NavigationSplitView基于selection机制复用detail视图,而非像旧NavigationView那样每次跳转重建视图,传递副本无法同步更新
  3. 原UnitModel中@AppStorage直接存储数组,无法正确发布变化通知,导致视图无法感知数据源更新

正确实现方案

步骤1:修正UnitModel的可观察性与数据持久化

struct Unit: Codable, Hashable, Identifiable {
    let id: UUID
    var filename: String // 补充缺失的filename属性
    var text: String
    var date = Date()
    var dateText: String {
        let df = DateFormatter()
        df.dateFormat = "EEEE, MMM d yyyy, h:mm a"
        return df.string(from: date)
    }
    var changed: Bool = false
}

final class UnitModel: ObservableObject {
    @AppStorage("unit") private var unitsData: Data = Data()
    @Published var units: [Unit] = []
    
    init() {
        // 从AppStorage解码数据
        if let decoded = try? JSONDecoder().decode([Unit].self, from: unitsData) {
            units = decoded.sorted(by: { $0.date > $1.date })
        }
    }
    
    // 保存数据到AppStorage
    func saveUnits() {
        if let encoded = try? JSONEncoder().encode(units) {
            unitsData = encoded
        }
    }
    
    func sortList() {
        units = units.sorted(by: { $0.date > $1.date })
        saveUnits()
    }
}

步骤2:ContentView中绑定选中的Unit实例

struct ContentView: View {
    @EnvironmentObject private var data: UnitModel
    @State private var selectedUnit: Unit?

    var body: some View {
        NavigationSplitView {
            // 使用绑定数组,让列表直接关联数据源
            List($data.units, selection: $selectedUnit) { $unit in
                VStack(alignment: .leading) {
                    Text(unit.filename)
                    Text(unit.dateText)
                }
            }
        } detail: {
            // 根据选中状态展示对应视图
            if let selectedUnit = $selectedUnit {
                UnitView(unit: selectedUnit)
            } else {
                Text("Select a unit.")
            }
        }
    }
}

步骤3:UnitView直接绑定Unit的text属性

struct UnitView: View {
    @Binding var unit: Unit
    @EnvironmentObject private var data: UnitModel

    var body: some View {
        VStack(alignment: .leading) {
            TextEditor(text: $unit.text)
                // 编辑文本时自动保存到数据源
                .onChange(of: unit.text) { _ in
                    data.saveUnits()
                }
        }
    }
}

方案说明

  • 用@Published修饰UnitModel的units数组,确保数据源变化能触发视图更新
  • 列表使用$data.units绑定,选中的是Unit的引用而非副本,直接同步到detail视图
  • UnitView通过@Binding绑定选中的Unit,编辑文本直接修改数据源,选中新单元时自动更新TextEditor内容
  • 修正AppStorage的存储方式,用Data序列化数组,避免直接存储Codable类型的潜在问题

内容的提问来源于stack exchange,提问作者Aleph

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.02 02:45:18