SwiftUI/Combine 通知用不正确的值覆盖了我的结构
SwiftUI/Combine notification overwrites my struct with incorrect values
我已将一个最小示例发布到 github。
This question 好像有关系,但是我没有取得任何进展。
我有一个包含 @Published
结构的 @ObservableObject
class。我有一个 Slider
绑定到该结构中的一个字段,并且该结构上有两个观察者。其中一名观察员对结构中的不同字段进行了更改。当我在通知期间读取更新的结构时,一切看起来都正确。通知后,我的结构已被覆盖。我无法在调试器中找到它发生的位置;它在 @main
上中断。我已经在我的真实应用程序中到处进行诊断,但无法弄清楚它在哪里被设置为错误的值。
似乎正在发生的事情是 SwiftUI 正在复制该结构。如果结构不是对象的一部分,我可以理解制作副本,但我不明白为什么要在这里这样做。
将我的结构更改为 class 会导致覆盖停止。但是我的结构真的不应该是 class;在我的示例中有点像 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 = [=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.main
或 DispatchQueue.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
部分,并小心地用原始值覆盖您在订阅者中所做的更改。
当您延迟订阅者时,它不会让订阅者立即进行更改,而是在主队列上发布一个块,稍后 运行 这样您就不会在正在更改滑块的“当前”事件处理周期。
我已将一个最小示例发布到 github。
This question 好像有关系,但是我没有取得任何进展。
我有一个包含 @Published
结构的 @ObservableObject
class。我有一个 Slider
绑定到该结构中的一个字段,并且该结构上有两个观察者。其中一名观察员对结构中的不同字段进行了更改。当我在通知期间读取更新的结构时,一切看起来都正确。通知后,我的结构已被覆盖。我无法在调试器中找到它发生的位置;它在 @main
上中断。我已经在我的真实应用程序中到处进行诊断,但无法弄清楚它在哪里被设置为错误的值。
似乎正在发生的事情是 SwiftUI 正在复制该结构。如果结构不是对象的一部分,我可以理解制作副本,但我不明白为什么要在这里这样做。
将我的结构更改为 class 会导致覆盖停止。但是我的结构真的不应该是 class;在我的示例中有点像 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 = [=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.main
或 DispatchQueue.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
部分,并小心地用原始值覆盖您在订阅者中所做的更改。
当您延迟订阅者时,它不会让订阅者立即进行更改,而是在主队列上发布一个块,稍后 运行 这样您就不会在正在更改滑块的“当前”事件处理周期。