Reputation: 1043
@StateObject
that is being used to drive a NavigationSplitView
.@Published
properties on the @StateObject
to drive the selection of the view. @StateObject + @Published
NavigationSplitView
I want to create a Selection
.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)
}
}
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()
}
}
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()
}
}
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.
Upvotes: 3
Views: 268
Reputation: 7745
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.
Upvotes: 1