Reputation: 615
I use two publishers in View:
A: String.publisher
B: ObservableObject include one @Published String type
If I monitoring publisher A,I get a infinite loop. But monitoring publisher B is OK!
import SwiftUI
import Combine
class Model: ObservableObject{
@Published var someBool = false
@Published var name:String = ""
}
struct ContentView: View {
// Publisher A
@State var name = ""
// Publisher B
@ObservedObject var model = Model()
var body: some View {
VStack {
// Plan A: lead to infinite loop!!!
TextField("Input Name", text: $name)
// Plan B: It's OK
//TextField("Input Name", text: $model.name)
.onReceive(name.publisher.reduce("", {t,c in
t + String(c)
})) {text in
print("change to \(text)")
self.model.someBool.toggle() //Plan A: infinite loop!!!
}
/*
.onReceive(model.$name){name in
print("change to \(name)")
self.model.someBool.toggle() //Plan B: It's OK!!!
}
*/
}
}
}
Although I changed model.someBool value in onReceive(),But Plan B is fine, Plan A lead to infinite loop. Why is That??? Thanks :)
Upvotes: 1
Views: 2983
Reputation: 17534
Hopy, you need one source of true. If you don't like to use your model, the equivalent code with State / Binding pair could looks like
struct ContentView: View {
@State var name: String = ""
@State var flag = false
var body: some View {
let subject = CurrentValueSubject<String, Never>(name)
return VStack {
TextField("Input Name", text: $name).textFieldStyle(RoundedBorderTextFieldStyle()).padding()
.onReceive(subject) { name in
print("change to \(name)")
self.flag.toggle() // toggle every char typing
}
}
}
}
In your example I disable (see the commented line) the default "request" in model
import SwiftUI
import Combine
class Model: ObservableObject{
var someBool = false {
willSet {
print("will change to", newValue)
print("ask SwiftUI to update from model")
//self.objectWillChange.send()
}
didSet {
print(oldValue, "changed")
}
}
}
struct ContentView: View {
@State var name = ""
@StateObject var model = Model()
var body: some View {
VStack {
TextField("Input Name", text: $name).textFieldStyle(RoundedBorderTextFieldStyle())
.onReceive(name.publisher.reduce("", {t,c in
t + String(c)
})) {text in
print("change to \(text)")
self.model.someBool.toggle()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
while typing it prints
true changed
change to Qw
will change to true
ask SwiftUI to update from model
false changed
change to Qwe
will change to false
ask SwiftUI to update from model
true changed
change to Qwer
will change to true
ask SwiftUI to update from model
false changed
change to Qwert
will change to false
ask SwiftUI to update from model
true changed
...
Now uncomment the line in your model
class Model: ObservableObject{
var someBool = false {
willSet {
print("will change to", newValue)
print("ask SwiftUI to update from model")
self.objectWillChange.send()
}
didSet {
print(oldValue, "changed")
}
}
}
and run it again ... it will print in infinite loop
...
change to
will change to true
ask SwiftUI to update from model
false changed
change to
will change to false
ask SwiftUI to update from model
true changed
change to
will change to true
ask SwiftUI to update from model
false changed
change to
will change to false
ask SwiftUI to update from model
true changed
change to
...
your model changes, SwiftUI is reevaluating its body and due this the model changes again ... in a loop.
The minimal loop example
import SwiftUI
import Combine
class Model: ObservableObject {
@Published var flag = false
}
struct ContentView: View {
@StateObject var model = Model()
var body: some View {
Color.yellow
.onReceive(model.$flag) {_ in
print(".")
self.model.flag.toggle()
}
}
}
Upvotes: 2