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

Posted

技术标签:

【中文标题】SwiftUI/Combine 通知用不正确的值覆盖我的结构【英文标题】:SwiftUI/Combine notification overwrites my struct with incorrect values 【发布时间】:2021-12-21 22:02:06 【问题描述】:

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

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

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

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

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

为什么它对结构会这样? 这是对 Combine、SwiftUI 或 Swift 的误用吗? 我在这里做的事情通常是不好的做法吗? 有更好的方法吗?

这是主应用模块:

@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 = $0
            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 
            $0.origin.x == $1.origin.x
        
        .sink  [weak self] in
            guard let myself = self else  return 
            myself.frame.origin.y = $0.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 
            $0.origin.x == $1.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 = $0.origin.x
            print("Writing (x: \(myself.frame.origin.x), y: \(myself.frame.origin.y))")
        

【问题讨论】:

不清楚 wrongright 值是什么意思,以及 struct has been overwritten... struct 在更改时总是覆盖,因为它是一个值,就像 1 更改为 2 - 完全不同的东西。 我的意思是,我正在将值“A”写入我的结构,当我读回它时,我正在读取值“B”。我在结尾和 cmets 中提到了它。请就如何改进我的问题给我建议;我看到有人投票关闭但没有给出任何反馈 这听起来像是“线程安全问题”,即非同步读/写。 DispatchQueue.global可以提供任何可用的队列,可以是主队列也可以是后台队列,未定义。 我的错。那应该是DispatchQueue.main。我已经改了。 但是,如果我使用.receive(on: DispatchQueue.main).subscribe(on: DispatchQueue.main),问题就完全消失了。感谢您的启发! 【参考方案1】:

查看documentation for the Published property wrapper

在那里你会找到句子

当属性发生变化时,会在属性的 willSet 中进行发布 块,意味着订阅者在新值实际出现之前收到它 在属性上设置

有一个不言而喻的警告,属性的值在订阅者收到新值后设置。

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

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

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

【讨论】:

所以问题在于从订阅者回调中写入我的对象。听起来不错,这与我的其他观察结果一致。非常感谢!

以上是关于SwiftUI/Combine 通知用不正确的值覆盖我的结构的主要内容,如果未能解决你的问题,请参考以下文章

SwiftUI+Combine - 动态订阅发布者的字典

SwiftUI + Combine:如何将数据分配给带有动画的模型

SwiftUI/Combine 发布者是双向的吗?

在需要@Binding 的地方传递@Published(SwiftUI、Combine)

使用 SwiftUI+Combine 的数据库延迟加载

swiftui combine 无法获取数据 [关闭]