@Binding 在 @StateObject 上发布两次与 View 有关吗?

huangapple go评论79阅读模式
英文:

@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 a NavigationSplitView.
  • 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 a Selection.
  • 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] = [&quot;Coffee&quot;,&quot;Tea&quot;,&quot;Beer&quot;].map { str in Consumption(name: str)}
    let freq: [Frequency] = [&quot;daily&quot;,&quot;weekly&quot;,&quot;monthly&quot;].map { str in Frequency(name: str)}

    @Published var consumption: Consumption?
    @Published var frequency: Frequency?
    @Published var selection: Selection?
    
    private var cancellable = Set&lt;AnyCancellable&gt;()

    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(&quot;THIS SHOULD APPEAR ONCE&quot;)
                    print(&quot;---------------&quot;)
                    self.selection = .init(consumption: self.consumption!, frequency: newValue)
                } else {
                    print(&quot;NOTHING&quot;)
                    print(&quot;---------------&quot;)
                }
            }.store(in: &amp;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: &quot;circle.fill&quot;)
                    }
                }
            }
        } content: {
            switch model.consumption {
            case .none:
                Text(&quot;nothing&quot;)
            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(&quot;nothing selected&quot;)
            case .some(let selection):
                VStack {
                    Text(selection.consumption.name)
                    Text(selection.frequency.name)
                }
            }
        }

    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

@Binding 在 @StateObject 上发布两次与 View 有关吗?

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: &quot;circle.fill&quot;)
                     }
                 }
             }.onChange(of: consumption) { newValue in
                 self.model.consumption = newValue
             }
         } content: {
             switch model.consumption {
             case .none:
                 Text(&quot;nothing&quot;)
             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(&quot;nothing selected&quot;)
             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(&quot;nothing&quot;)
case .some(let consumption):
// Create a custom binding and connect it to the viewmodel
let binding = Binding&lt;Frequency?&gt; {
model.frequency
} set: { frequency, transaction in
model.frequency = frequency
// this will print after the List selected a new Value
print(&quot;List set frequency&quot;)
}
// 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: &quot;daily&quot;)))
THIS SHOULD APPEAR ONCE
---------------
List set frequency
receive value: (Optional(__lldb_expr_7.Frequency(id: D9552E6A-71FE-407A-90F5-87C39FE24193, name: &quot;daily&quot;)))
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.

huangapple
  • 本文由 发表于 2023年2月14日 00:57:31
  • 转载请务必保留本文链接:https://go.coder-hub.com/75438974.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定