SwiftUI/Combine 通知用不正确的值覆盖了我的结构

SwiftUI/Combine notification overwrites my struct with incorrect values

我已将一个最小示例发布到 github

This question 好像有关系,但是我没有取得任何进展。

我有一个包含 @Published 结构的 @ObservableObject class。我有一个 Slider 绑定到该结构中的一个字段,并且该结构上有两个观察者。其中一名观察员对结构中的不同字段进行了更改。当我在通知期间读取更新的结构时,一切看起来都正确。通知后,我的结构已被覆盖。我无法在调试器中找到它发生的位置;它在 @main 上中断。我已经在我的真实应用程序中到处进行诊断,但无法弄清楚它在哪里被设置为错误的值。

似乎正在发生的事情是 SwiftUI 正在复制该结构。如果结构不是对象的一部分,我可以理解制作副本,但我不明白为什么要在这里这样做。

将我的结构更改为 class 会导致覆盖停止。但是我的结构真的不应该是 class;在我的示例中有点像 CGRect。所以我想知道:

这是主要的应用程序模块:

@ObservedObject var arena = Arena()

var body: some Scene {
    WindowGroup {
        ContentView(arena: arena).onAppear { arena.postInit() }
    }
}

这里是ContentView

@ObservedObject var arena: Arena

func bind() -> Binding<Double> {
    // A sanity check of sorts, to verify that I'm really
    // reading and writing the slider values.
    Binding(
        get: { arena.frame.origin.x },
        set: {
            arena.frame.origin.x = [=12=]
            print("Binding (x: \(arena.frame.origin.x), y: \(arena.frame.origin.y))")
        }
    )
}

var body: some View {
    // Slide this slider around; it will write to the X in the
    // frame struct.
    Slider(
        value: bind(), in: -1.0...1.0,
        label: { Text("Origin.x \(arena.frame.origin.x)") }
    )

    Button("Check values") {
        print("Button (x: \(arena.frame.origin.x), y: \(arena.frame.origin.y))")
    }
}

这里是Arena

@Published var frame: CGRect = .zero

var xObserver: AnyCancellable?

func postInit() {
    // Whenever the X is changed, update the Y
    xObserver = $frame
        .removeDuplicates {
            [=13=].origin.x == .origin.x
        }
        .sink { [weak self] in
            guard let myself = self else { return }
            myself.frame.origin.y = [=13=].origin.x
            print("Writing (x: \(myself.frame.origin.x), y: \(myself.frame.origin.y))")
        }
}

如果您 运行 应用程序并四处移动滑块,您可以在控制台输出中看到我正在写入矩形中的 x/y 值,但是当您单击按钮时你可以看到我们正在读取矩形值并得到零。

一个线索是,如果我如下所示将 RunLoop.mainDispatchQueue.main 添加到我的观察者,则在单击按钮时会读回正确的值,尽管在滑块时它们不正确正在四处移动。

// In the Arena class
func postInit() {
    xObserver = $frame
        .removeDuplicates {
            [=14=].origin.x == .origin.x
        }

        // Adding this line changes the behavior; it
        // doesn't seem to clear up the whole issue, but it's a
        // clue. Note that this works the same using
        // DispatchQueue.main, but not using ImmediateScheduler.shared
        .receive(on: RunLoop.main)

        .sink { [weak self] in
            guard let myself = self else { return }
            myself.frame.origin.y = [=14=].origin.x
            print("Writing (x: \(myself.frame.origin.x), y: \(myself.frame.origin.y))")
        }
}

documentation for the Published property wrapper

中查看

在那里你会找到句子:

When the property changes, publishing occurs in the property’s willSet block, meaning subscribers receive the new value before it’s actually set on the property

需要注意的是,属性 的值将在订阅者收到新值后设置

因此您的 $frame 发布者在 frame 属性 的 willSet 中触发,并且它传递了发布发生后框架将设置为的值。您的订阅者更改对象的框架,并在“Writing”字符串中打印新值。

但是一旦发布完成,系统会执行 属性 更改的 set 部分,并小心地用原始值覆盖您在订阅者中所做的更改。

当您延迟订阅者时,它不会让订阅者立即进行更改,而是在主队列上发布一个块,稍后 运行 这样您就不会在正在更改滑块的“当前”事件处理周期。