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

SwiftUI中实现openWindow数据绑定,同步InputView与ResultView

多窗口SwiftUI数据同步解决方案

问题分析

当前代码里,ResultView的Model是局部的@StateObject,打开InputWindow时传递的是model.calculation的值副本(因为Calculation是值类型),所以在InputView中修改input只会改动副本,无法同步回ResultView的原Model,导致视图无法实时更新。而弹窗(popover)能正常工作,是因为它共享同一个环境的状态,不存在值传递的副本问题。

要实现两个独立窗口的实时同步,核心是让两个窗口共享同一个数据源,避免值副本的隔离问题。

解决方案:全局共享ObservableObject

将Model提升到App级别作为全局状态,通过EnvironmentObject传递给所有需要的视图,同时让InputView直接绑定到全局Model的calculation属性,确保修改能实时同步到所有关联视图。

修改后的完整代码

import SwiftUI

@main
struct TestApp: App {
    
    @AppStorage("exponent") var exponent = 3
    @AppStorage("expList") var expList = [3]
    // 全局共享的Model,用@StateObject管理生命周期
    @StateObject private var model = Model(exponent: 3)
    
    var body: some Scene {
        WindowGroup {
            ResultView()
                .environmentObject(model) // 注入全局Model到环境
                // 监听exponent变化,更新Model中的指数
                .onChange(of: exponent) { newExp in
                    model.updateExponent(to: Double(newExp))
                }
        }
        // 直接打开InputWindow,传递全局Model的calculation绑定
        WindowGroup("Input Window", id: "inputWindow") {
            InputView(calculation: $model.calculation)
        }
       .commands{
            CommandMenu("Tools"){
                Menu("Exponents"){
                    Picker("",selection: $exponent){
                        ForEach(expList, id:\.self) { exp in
                            Text(String(exp)).tag(exp)
                        }
                    }
                    .pickerStyle(.inline)
                    .labelsHidden()
                    Divider()
                    Button("Add"){
                        expList = expList.sorted()
                        let newExp = expList.last! + 1
                        expList.append(newExp)
                    }
                }
            }
        }
    }
}

class Model: ObservableObject {
    
    @Published public var calculation: Calculation
    
    init(exponent: Double) {
        self.calculation = Calculation(exponent: exponent)
    }
    
    // 更新指数时保留当前输入值
    func updateExponent(to newExp: Double) {
        calculation = Calculation(exponent: newExp, input: calculation.input)
    }
}

struct Calculation: Codable, Hashable {
    
    var exponent: Double
    var input: Double = 2
    // 计算属性,input或exponent变化时自动更新结果
    var result: Double {
        return pow(input, exponent)
    }
    
    init(exponent: Double) {
        self.exponent = exponent
    }
    
    // 新增初始化方法,切换指数时保留现有输入值
    init(exponent: Double, input: Double) {
        self.exponent = exponent
        self.input = input
    }
}

struct ResultView: View {
    
    // 从环境中获取全局Model
    @EnvironmentObject private var model: Model
    @Environment(\.openWindow) private var openWindow
    
    var body: some View {
        VStack {
            Button("Set Input"){
                openWindow(id:"inputWindow")
            }
            Text("Exponent: \(model.calculation.exponent)")
            Text("Input: \(model.calculation.input)")
            Text("Result: \(model.calculation.result)")
        }
        .frame(width: 200, height: 200)
    }
}

struct InputView: View {
    // 直接绑定全局Model的calculation属性
    @Binding var calculation: Calculation
    
    var body: some View {
        HStack{
            Text("Input: ")
            TextField("", value: $calculation.input, format: .number)
        }
        .padding()
    }
}

extension Array: RawRepresentable where Element: Codable {
    public init?(rawValue: String) {
        guard let data = rawValue.data(using: .utf8),
              let result = try? JSONDecoder().decode([Element].self, from: data)
        else {
            return nil
        }
        self = result
    }
    
    public var rawValue: String {
        guard let data = try? JSONEncoder().encode(self),
              let result = String(data: data, encoding: .utf8)
        else {
            return "[]"
        }
        return result
    }
}

关键修改点

  1. 全局Model:在TestApp中用@StateObject声明全局共享的Model,确保整个App生命周期内只有一个实例,避免状态分散。
  2. 环境注入:ResultView通过@EnvironmentObject获取全局Model,不再局部创建实例,实现状态共享。
  3. 绑定传递:打开InputWindow时直接传递$model.calculation的绑定,InputView修改的是全局Model中的属性,而非独立副本。
  4. 指数更新逻辑:新增updateExponent方法,切换指数时保留当前输入值,提升用户操作连贯性。

扩展方案:复杂数据用ObservableObject

如果你的Calculation包含更多复杂属性,可以将它改为ObservableObject,这样内部属性变化时能更精准地触发视图更新,无需重新创建实例:

class Calculation: ObservableObject, Codable, Hashable {
    
    @Published var exponent: Double
    @Published var input: Double = 2
    var result: Double {
        return pow(input, exponent)
    }
    
    init(exponent: Double) {
        self.exponent = exponent
    }
    
    // 实现Codable协议
    enum CodingKeys: String, CodingKey {
        case exponent, input
    }
    
    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        exponent = try container.decode(Double.self, forKey: .exponent)
        input = try container.decodeIfPresent(Double.self, forKey: .input) ?? 2
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(exponent, forKey: .exponent)
        try container.encode(input, forKey: .input)
    }
    
    // 实现Hashable协议
    static func == (lhs: Calculation, rhs: Calculation) -> Bool {
        lhs.exponent == rhs.exponent && lhs.input == rhs.input
    }
    
    func hash(into hasher: inout Hasher) {
        hasher.combine(exponent)
        hasher.combine(input)
    }
}

// 对应的Model更新
class Model: ObservableObject {
    
    @Published public var calculation: Calculation
    
    init(exponent: Double) {
        self.calculation = Calculation(exponent: exponent)
    }
    
    func updateExponent(to newExp: Double) {
        calculation.exponent = newExp
    }
}

这种方式下,修改input时@Published会直接触发视图更新,更适合复杂状态的精细化管理。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.30 08:09:51