More on assign(to:)
July 24, 2020
Yesterday I said "I don't know how I feel about the new &." I complained, "It feels clunky." I've decided it's more than that. It feels wrong to insist on an inout parameter. It is out of place with the mental model I have for publishers as you'll see in the example below and it also fights the way we use bindings. I would expect assign(to:) to feel the same.
That said, I don't expect that anyone cares what I think so I'm just going to have to buck up and learn to like it.
Here is a short example of how to use the new assign(to:) followed by a brief discussion of what I don't like about the &.
Create an iOS playground in Xcode 12 beta 3 and paste this code into it. The code includes a SwiftUI View named MyView, a model object named Model and a thing in the middle that subscribes to the model's publisher, transforms the Int to a String and passes it on to the Publisher named text.
It's only a few dozen lines long, but I've highlighted the important part.
import Combine
import SwiftUI
struct MyView: View {
@ObservedObject var presenter = Presenter()
var body: some View {
VStack {
Text(presenter.text)
Button("Next",
action: presenter.action)
}
}
}
class Presenter: ObservableObject {
private let model = Model()
lazy var action = model.next
@Published var text: String = "Nothing yet"
init() {
model.$value
.map(\.description)
.assign(to: &$text)
}
}
class Model: ObservableObject {
@Published var value: Int = 0
func next() {
value = Int.random(in: (0...9))
}
}
import PlaygroundSupport
PlaygroundPage.current.setLiveView(MyView())
Run this code in the playground and you can tap the button to fetch a new random number.
The body of Presenter's init() contains the code where we subscribe to the model's publisher $value, transform it to a String and use assign(to:).
init() {
model.$value
.map(\.description)
.assign(to: &$text)
}
If you look at the implementation for Model you can see an argument for why the highlighted line should be
.assign(to: text)
After all, in the Model the next() method modifies the Int and it is the publishers job to send out this new value to all that are interested because $value is a Publisher of Ints that never fails.
In the Model we don't modify $value, we modify value (which is magical in the same way that @State and @Binding are), and the new value is published.
Putting that aside, the way we use bindings argues in favor of using this in assign(to:).
.assign(to: $text)
This feels like the way in which we use a binding in a Picker or Slider. We pass in the $intValue or $floatValue to the Picker or Slider respectively and not the intValue or floatValue. Internally the code that is used might use &$intValue or &$floatValue but we are just responsible for passing on the variable the controller is bound to.
In the same way, with assign(to:) my mental model is that we are not changing the publisher, we are providing the next value to be sent into the stream. Under the covers the publisher needs to let people know there's a new value so the publisher has changed - but we aren't changing the publisher with assign(to:) any more than we change it in next().
It could be that I deeply misunderstand this, but after living with this for a day and comparing the way I use assign(to:) to other Combine and SwiftUI patterns, the use of the inout parameter in assign(to:) feels odd to me.