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
。所以我想知道:
这是主应用模块:
@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.main
或DispatchQueue.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))")
【问题讨论】:
不清楚 wrong 和 right 值是什么意思,以及 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:如何将数据分配给带有动画的模型