2

I have attached an onReceive modifier to a view in SwiftUI. The purpose of the subscription is to respond to changes in an @Published property greeting, part of a view Model object.

The view contains a Segmented Picker. The segmented picker uses another property in the view model -- index.

Inexplicably, when user changes the segmented picker selection the onReceive block is called, even though the Publisher greeting has not changed.


import SwiftUI
import Combine


class Model: ObservableObject {
    @Published var selectedIndex = 0
    @Published var greeting = "initial"
    
}

struct ContentView: View {
    @ObservedObject var model: Model
    let pickerTitles = ["One", "Two", "Three"]
    
    var body: some View {
        VStack {
            return Picker("Options", selection: $model.selectedIndex) {
                ForEach(0 ..< pickerTitles.count) { index in
                    Text(self.pickerTitles[index])
                }
                
            }.pickerStyle(SegmentedPickerStyle())
        }
        .onReceive([model.greeting].publisher){str in
            print(str)
        }
   
    }
}

What's going on? Above code is the entirety of the codebase.

RE: SUMMARY I specify a Publisher in .onReceive block. That specified property -- greeting -- never changes. When another property -- index -- is changed by the Picker in the same ObservedObject, the .onReceive block is mysteriously called.

enter image description here

1 Answer 1

0

You use Publishers.Sequence in your code, which publishes the elements in the sequence one by one.

And with your code, [model.greeting].publisher is re-evaluated and the publisher is re-created at each time body is evaluated, which means the published element reset to the first.

With the following code, you can find that x is printed out, at each time body is evaluated:

struct ContentView: View {
    @ObservedObject var model: Model
    let pickerTitles = ["One", "Two", "Three"]

    var body: some View {
        VStack {
            return Picker("Options", selection: $model.selectedIndex) {
                ForEach(0 ..< pickerTitles.count) { index in
                    Text(self.pickerTitles[index])
                }

            }.pickerStyle(SegmentedPickerStyle())
        }
        .onReceive(["x"].publisher){str in
            print(str)
        }
    }
}

If you want to receive the changes only on greeting, you can write something like this:

struct ContentView: View {
    @ObservedObject var model: Model
    let pickerTitles = ["One", "Two", "Three"]

    var body: some View {
        VStack {
            return Picker("Options", selection: $model.selectedIndex) {
                ForEach(0 ..< pickerTitles.count) { index in
                    Text(self.pickerTitles[index])
                }

            }.pickerStyle(SegmentedPickerStyle())
        }
        .onReceive(model.$greeting){str in
            print(str)
        }
    }
}
Sign up to request clarification or add additional context in comments.

5 Comments

I have to think on this, but I think I'm understanding your point that every time the user selects a different segment in the Picker, the Combine library observes the change in the view model, and nukes the original View. A new View struct re-init'd. Since it appears that .publisher defaults to a CurrentSubject publisher, the initial value ("initial value") gets emitted when the new View struct is created. THANK YOU. I'll play around. As an aside, it's tricky moving to the world of value objects. In UIKit almost everything is a reference object and those don't 'disappear' suddenly.
@SmallTalk, I'm not sure whether you get the point correctly, but your .publisher is put on an Array, so it has nothing to do with CurrentValueSubject. Your .publisher calls the publisher property on Sequence. Your code [model.greeting].publisher ignores all the published or observable features of your model and creating a Publishers.Sequence from ["initial"].
Clear I have more understanding gaps. Thanks for comment. Only point I want to clarify right now is: does a View struct get nuked and re-init'd from scratch if State -- or Published property in an ObservedObject --changes. Seems like the answer is YES. (I understand there some magic where SwiftUI will preserve current values of State, and also maybe 'reconnect' the reference to an ObservedObject in between trashing a View and recreating from scratch with preserved values. It's a weird new world in SwiftUI.
@SmallTalk, seems you got to the right point. SwiftUI evaluates body at any time it thinks is needed. The view returned is sort of a blueprint and SwiftUI compares it to the old body, and updates the actual visual components which have changed. Old programmers like me may need plenty of time to be accustomed to it...
THANK YOU FOR HELPING. I can now move forward with slightly better understanding.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.