英文:
@Binding is being published twice on @StateObject vs View?
问题
以下是您要翻译的代码部分:
import SwiftUI
import Combine
struct Consumption: Identifiable, Hashable {
var id: UUID = UUID()
var name: String
}
struct Frequency: Identifiable, Hashable {
var id: UUID = UUID()
var name: String
}
struct Selection: Identifiable {
var id: UUID = UUID()
var consumption: Consumption
var frequency: Frequency
}
class SomeModel: ObservableObject {
let consump: [Consumption] = ["Coffee","Tea","Beer"].map { str in Consumption(name: str)}
let freq: [Frequency] = ["daily","weekly","monthly"].map { str in Frequency(name: str)}
@Published var consumption: Consumption?
@Published var frequency: Frequency?
@Published var selection: Selection?
private var cancellable = Set<AnyCancellable>()
init(consumption: Consumption? = nil, frequency: Frequency? = nil, selection: Selection? = nil) {
self.consumption = consumption
self.frequency = frequency
self.selection = selection
$frequency
.print()
.sink { newValue in
if let newValue = newValue {
print("THIS SHOULD APPEAR ONCE")
print("---------------")
self.selection = .init(consumption: self.consumption!, frequency: newValue)
} else {
print("NOTHING")
print("---------------")
}
}.store(in: &cancellable)
}
}
struct ContentView: View {
@StateObject var model: SomeModel = .init()
var body: some View {
NavigationSplitView {
VStack {
List(model.consump, selection: $model.consumption) { item in
NavigationLink(value: item) {
Label(item.name, systemImage: "circle.fill")
}
}
}
} content: {
switch model.consumption {
case .none:
Text("nothing")
case .some(let consumption):
List(model.freq, id:\.id, selection: $model.frequency) { item in
NavigationLink(item.name, value: item)
}
.navigationTitle(consumption.name)
}
} detail: {
switch model.selection {
case .none:
Text("nothing selected")
case .some(let selection):
VStack {
Text(selection.consumption.name)
Text(selection.frequency.name)
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
希望这有助于您的工作。如果您有任何其他问题,请随时提出。
英文:
Overview
- I have a
@StateObject
that is being used to drive aNavigationSplitView
. - I am using the
@Published
properties on the@StateObject
to drive the selection of the view.@StateObject + @Published
- When I get to the final selection of the
NavigationSplitView
I want to create aSelection
. - This should publish a property only once.
import SwiftUI
import Combine
struct Consumption: Identifiable, Hashable {
var id: UUID = UUID()
var name: String
}
struct Frequency: Identifiable, Hashable {
var id: UUID = UUID()
var name: String
}
struct Selection: Identifiable {
var id: UUID = UUID()
var consumption: Consumption
var frequency: Frequency
}
class SomeModel: ObservableObject {
let consump: [Consumption] = ["Coffee","Tea","Beer"].map { str in Consumption(name: str)}
let freq: [Frequency] = ["daily","weekly","monthly"].map { str in Frequency(name: str)}
@Published var consumption: Consumption?
@Published var frequency: Frequency?
@Published var selection: Selection?
private var cancellable = Set<AnyCancellable>()
init(consumption: Consumption? = nil, frequency: Frequency? = nil, selection: Selection? = nil) {
self.consumption = consumption
self.frequency = frequency
self.selection = selection
$frequency
.print()
.sink { newValue in
if let newValue = newValue {
print("THIS SHOULD APPEAR ONCE")
print("---------------")
self.selection = .init(consumption: self.consumption!, frequency: newValue)
} else {
print("NOTHING")
print("---------------")
}
}.store(in: &cancellable)
}
}
Problem
The @StateObject
on the view is publishing the property twice. (See code below)
struct ContentView: View {
@StateObject var model: SomeModel = .init()
var body: some View {
NavigationSplitView {
VStack {
List(model.consump, selection: $model.consumption) { item in
NavigationLink(value: item) {
Label(item.name, systemImage: "circle.fill")
}
}
}
} content: {
switch model.consumption {
case .none:
Text("nothing")
case .some(let consumption):
List(model.freq, id:\.id, selection: $model.frequency) { item in
NavigationLink(item.name, value: item)
}
.navigationTitle(consumption.name)
}
} detail: {
switch model.selection {
case .none:
Text("nothing selected")
case .some(let selection):
VStack {
Text(selection.consumption.name)
Text(selection.frequency.name)
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
My Solution
If I change the View by creating @State
properties on the view and then use OnChange
modifier to update the model I get the property to update once. Which is what I want. (see code below)
struct ContentView: View {
@StateObject var model: SomeModel = .init()
@State private var consumption: Consumption?
@State private var frequency: Frequency?
var body: some View {
NavigationSplitView {
VStack {
List(model.consump, selection: $consumption) { item in
NavigationLink(value: item) {
Label(item.name, systemImage: "circle.fill")
}
}
}.onChange(of: consumption) { newValue in
self.model.consumption = newValue
}
} content: {
switch model.consumption {
case .none:
Text("nothing")
case .some(let consumption):
List(model.freq, id:\.id, selection: $frequency) { item in
NavigationLink(item.name, value: item)
}
.navigationTitle(consumption.name)
.onChange(of: frequency) { newValue in
self.model.frequency = newValue
}
}
} detail: {
switch model.selection {
case .none:
Text("nothing selected")
case .some(let selection):
VStack {
Text(selection.consumption.name)
Text(selection.frequency.name)
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Question(s) / Help
Is the behaviour of @Binding
different between @StateObject + @Published
vs View + @State
?
I don't understand why in my previous View the property was being published twice.
My solution is a bit more work but it works by using @State
and onChange
view modifiers.
答案1
得分: 1
以下是翻译好的内容:
"Well I can´t completely explain why this happens. It seems that List
updates its selection twice.
为什么?不明确。可能是一个bug吗?。
Consider this debugging approach:
考虑以下的调试方法:
} content: {
switch model.consumption {
case .none:
Text("nothing")
case .some(let consumption):
// Create a custom binding and connect it to the viewmodel
let binding = Binding<Frequency?> {
model.frequency
} set: { frequency, transaction in
model.frequency = frequency
// this will print after the List selected a new Value
print("List set frequency")
}
// use the custom binding here
List(model.freq, id:.id, selection: binding) { item in
NavigationLink(item.name, value: item)
}
.navigationTitle(consumption.name)
}
This will produce the following output:
这将产生以下输出:
receive subscription: (PublishedSubject)
接收订阅:(PublishedSubject)
request unlimited
请求无限
receive value: (nil)
接收数值:(nil)
NOTHING
什么都没有
receive value: (Optional(__lldb_expr_7.Frequency(id: D9552E6A-71FE-407A-90F5-87C39FE24193, name: "daily")))
接收数值:(Optional(__lldb_expr_7.Frequency(id: D9552E6A-71FE-407A-90F5-87C39FE24193, name: "daily"))
THIS SHOULD APPEAR ONCE
这应该出现一次
List set frequency
List设置频率
receive value: (Optional(__lldb_expr_7.Frequency(id: D9552E6A-71FE-407A-90F5-87C39FE24193, name: "daily")))
接收数值:(Optional(__lldb_expr_7.Frequency(id: D9552E6A-71FE-407A-90F5-87C39FE24193, name: "daily"))
THIS SHOULD APPEAR ONCE
这应该出现一次
List set frequency
List设置频率
The reason you are not seeing this while using @State
and .onChange
is the .onChange
modifier. It fires only if the value changes which it does not. On the other hand your Combine
publisher fires every time no matter the value changed or not.
原因是你在使用@State
和.onChange
时没有看到这一点是因为.onChange
修饰符。它只在值更改时触发,而实际上值并没有更改。另一方面,你的Combine
发布者在值更改与否都会触发。
Solution:
解决方案:
You can keep your Combine approach and use the .removeDuplicates
modifier.
你可以保持使用Combine方法,然后使用.removeDuplicates
修饰符。
$frequency
.removeDuplicates()
.print()
.sink { newValue in
This will ensure the sink will only get called when new values are sent.
这将确保仅在发送新值时调用sink。
英文:
Well I can´t completely explain why this happens. It seems that List
updates its selection twice.
Why? Unknown. ?May be a bug?.
Consider this debugging approach:
} content: {
switch model.consumption {
case .none:
Text("nothing")
case .some(let consumption):
// Create a custom binding and connect it to the viewmodel
let binding = Binding<Frequency?> {
model.frequency
} set: { frequency, transaction in
model.frequency = frequency
// this will print after the List selected a new Value
print("List set frequency")
}
// use the custom binding here
List(model.freq, id:\.id, selection: binding) { item in
NavigationLink(item.name, value: item)
}
.navigationTitle(consumption.name)
}
This will produce the following output:
receive subscription: (PublishedSubject)
request unlimited
receive value: (nil)
NOTHING
---------------
receive value: (Optional(__lldb_expr_7.Frequency(id: D9552E6A-71FE-407A-90F5-87C39FE24193, name: "daily")))
THIS SHOULD APPEAR ONCE
---------------
List set frequency
receive value: (Optional(__lldb_expr_7.Frequency(id: D9552E6A-71FE-407A-90F5-87C39FE24193, name: "daily")))
THIS SHOULD APPEAR ONCE
---------------
List set frequency
The reason you are not seeing this while using @State
and .onChange
is the .onChange
modifier. It fires only if the value changes which it does not. On the other hand your Combine
publisher fires every time no matter the value changed or not.
Solution:
You can keep your Combine approach and use the .removeDuplicates
modifier.
$frequency
.removeDuplicates()
.print()
.sink { newValue in
This will ensure the sink will only get called when new values are sent.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论