SwiftUI and Combine what is the difference between Published and Binding - binding

I know in which situations to use #Binding and #Published
like in ObservableObject I generally use #Published, or objectWillChange.send()
And #Binding in subviews to propagate changes to parent
But here I have snippet which seems to be working that uses both #Binding and #Published
in ObservableObject
I consider what is the difference.
#Binding var input: T
#Binding var validation: Validation
#Published var value: T {
didSet {
self.input = self.value
self.validateField()
}
}
init(input: Binding<T>, rules: [Rule<T>], validation: Binding<Validation>) {
self._input = input
self.value = input.wrappedValue
self.rules = rules
self._validation = validation
}
As I tested it seems that if I bind TextField to #Published then didSet is called but if I bind it to #Binding then didSet won't be called.

For a #Binding, I found that didSet was called if you assign to the property directly. Try running this example in an xcode playground:
import SwiftUI
import Combine
struct MyView: View {
#Binding var value: Int {
didSet {
print("didSet", value)
}
}
var body: some View { Text("Hello") }
}
var myBinding = Binding<Int>(
get: { 4 },
set: { v in print("set", v)})
let view = MyView(value: myBinding)
view.value = 99
Output:
set 99
didSet 4
Regarding the difference between #Published and #Binding:
You'll generally use #Binding to pass a binding that originated from some source of truth (like #State) down the view hierarchy.
Use #Published in an ObservableObject to allow a view to react to changes to a property.

Related

How to trigger #State object with respect to ObservableObject Object?

This are two properties which I declared
struct DashBoardView: View {
#State var isToPush: Bool = false
#ObservedObject var sideBarHandler = SideBarHandler()
Where SideBarHandler is -
class SideBarHandler: ObservableObject {
#Published var isListItemClicked:Bool = false
}
Now I am looking to activate $isToPush based on sideBarHandler.isListItemClicked
Because I want to bind it here
NavigationLink(destination: FavouriteView(), isActive: $isToPush) {// NavLink
You have to consider where the source of truth is for the data.
Based on your description, it seems that the isListItemClicked is the source of truth, so you shouldn't even need a #State variable - use $sideBarHandler.isListItemClicked directly (prefix $ of an #ObservedObject gives you a Binding):
NavigationLink(destination: FavouriteView(), isActive: $sideBarHandler.isListItemClicked)
Of course, if #State var isToPush: Bool is only affected by sideBarHandler.isListItemClicked but otherwise exists independently - i.e. it is a source of truth for this data - then you can use onReceive as suggested by #Asperi to change the isToPush property:
.onReceive(sideBarHandler.$isListItemClicked) {
isToPush = $0
}
(note that $ prefix here accesses a #Published Combine publisher)
Also, unrelated to your question, but if you're instantiating an ObservableObject inside the view, then you should use #StateObject instead of #ObservedObject. The latter is meant for a case when the observable object is created outside of the view.
You can use onReceive somewhere in body, like
struct DashBoardView: View {
#State var isToPush: Bool = false
#ObservedObject var sideBarHandler = SideBarHandler()
var body: some View {
VStack {
// some content here
}
.onReceive(sideBarHandler.$isListItemClicked) {
isToPush = $0
}
}
}

SwiftUI Environment Object Not Getting Updated

I thought it was the case that #Published properties inside a class designated as an environment object would automatically update to all the subviews I pass the environment object to, but the below code isn't updating, what am I doing wrong?
class trackerDataStore: ObservableObject {
let healthStore = HKHealthStore()
#Published var isLoadingWorkouts = false
#Published var subscriptionIsActive = false
#Published var trackerWorkoutObject: trackerWorkoutObject?
}
struct detailHeaderCard: View {
#EnvironmentObject var trackerDataStore: TrackerDataStore
var body: some View {
//omitted
}
.sheet(isPresented: $isShowingPlayerAddStatsFormAsModal) {
PlayerAddStatsForm(isShowingPlayerAddStatsFormAsModal: $isShowingPlayerAddStatsFormAsModal)
.environmentObject(trackerDataStore)
}
}
struct PlayerAddStatsForm: View {
#EnvironmentObject var trackerDataStore: TrackerDataStore
//not getting reactively updated here
}
I found out that the way I wired up #EnvironmentObject actually works well, I had a bug in my code in another area that was preventing the view from receiving the data I was looking for that made me think it was related to #EnvironmentObject not working properly.

Cannot convert value of type 'Published<Bool>.Publisher' to expected argument type 'Binding<Bool>'

When trying to compile the following code:
class LoginViewModel: ObservableObject, Identifiable {
#Published var mailAdress: String = ""
#Published var password: String = ""
#Published var showRegister = false
#Published var showPasswordReset = false
private let applicationStore: ApplicationStore
init(applicationStore: ApplicationStore) {
self.applicationStore = applicationStore
}
var passwordResetView: some View {
PasswordResetView(isPresented: $showPasswordReset) // This is where the error happens
}
}
Where PasswordResetView looks like this:
struct PasswordResetView: View {
#Binding var isPresented: Bool
#State var mailAddress: String = ""
var body: some View {
EmptyView()
}
}
}
I get the error compile error
Cannot convert value of type 'Published<Bool>.Publisher' to expected argument type 'Binding<Bool>'
If I use the published variable from outside the LoginViewModel class it just works fine:
struct LoginView: View {
#ObservedObject var viewModel: LoginViewModel
init(viewModel: LoginViewModel) {
self.viewModel = viewModel
}
var body: some View {
PasswordResetView(isPresented: self.$viewModel.showPasswordReset)
}
}
Any suggestions what I am doing wrong here? Any chance I can pass a published variable as a binding from inside the owning class?
Thanks!
Not sure why the proposed solutions here are so complex, when there is a very direct fix for this.
Found this answer on a similar Reddit question:
The problem is that you are accessing the projected value of an #Published (which is a Publisher) when you should instead be accessing the projected value of an #ObservedObject (which is a Binding), that is: you have globalSettings.$tutorialView where you should have $globalSettings.tutorialView.
** Still new to Combine & SwiftUI so not sure if there is better way to approach **
You can initalize Binding from publisher.
https://developer.apple.com/documentation/swiftui/binding/init(get:set:)-6g3d5
let binding = Binding(
get: { [weak self] in
(self?.showPasswordReset ?? false)
},
set: { [weak self] in
self?.showPasswordReset = $0
}
)
PasswordResetView(isPresented: binding)
I think the important thing to understand here is what "$" does in the Combine context.
What "$" does is to publish the changes of the variable "showPasswordReset" where it is being observed.
when it precedes a type, it doesn't represent the type you declared for the variable (Boolean in this case), it represents a publisher, if you want the value of the type, just remove the "$".
"$" is used in the context where a variable was marked as an #ObservedObject,
(the ObservableObject here is LoginViewModel and you subscribe to it to listen for changes in its variables market as publishers)
struct ContentView: View {
#ObservedObject var loginViewModel: LoginViewModel...
in that context (the ContentView for example) the changes of "showPasswordReset" are going to be 'Published' when its value is updated so the view is updated with the new value.
Here is possible approach - the idea to make possible observation in generated view and avoid tight coupling between factory & presenter.
Tested with Xcode 12 / iOS 14 (for older systems some tuning might be needed)
protocol ResetViewModel {
var showPasswordReset: Bool { get set }
}
struct PasswordResetView<Model: ResetViewModel & ObservableObject>: View {
#ObservedObject var resetModel: Model
var body: some View {
if resetModel.showPasswordReset {
Text("Show password reset")
} else {
Text("Show something else")
}
}
}
class LoginViewModel: ObservableObject, Identifiable, ResetViewModel {
#Published var mailAdress: String = ""
#Published var password: String = ""
#Published var showRegister = false
#Published var showPasswordReset = false
private let applicationStore: ApplicationStore
init(applicationStore: ApplicationStore) {
self.applicationStore = applicationStore
}
var passwordResetView: some View {
PasswordResetView(resetModel: self)
}
}
For error that states: "Cannot convert value of type 'Binding' to expected argument type 'Bool'" solution is to use wrappedValue as in example below.
If you have MyObject object with property isEnabled and you need to use that as vanilla Bool type instead of 'Binding' then do this
myView.disabled($myObject.isEnabled.wrappedValue)

Embedded #Binding does not changing a value

Following this cheat sheet I'm trying to figure out data flow in SwiftUI. So:
Use #Binding when your view needs to mutate a property owned by an ancestor view, or owned by an observable object that an ancestor has a reference to.
And that is exactly what I need so my embedded model is:
class SimpleModel: Identifiable, ObservableObject {
#Published var values: [String] = []
init(values: [String] = []) {
self.values = values
}
}
and my View has two fields:
struct SimpleModelView: View {
#Binding var model: SimpleModel
#Binding var strings: [String]
var body: some View {
VStack {
HStack {
Text(self.strings[0])
TextField("name", text: self.$strings[0])
}
HStack {
Text(self.model.values[0])
EmbeddedView(strings: self.$model.values)
}
}
}
}
struct EmbeddedView: View {
#Binding var strings: [String]
var body: some View {
VStack {
TextField("name", text: self.$strings[0])
}
}
}
So I expect the view to change Text when change in input field will occur. And it's working for [String] but does not work for embedded #Binding object:
Why it's behaving differently?
Make property published
class SimpleModel: Identifiable, ObservableObject {
#Published var values: [String] = []
and model observed
struct SimpleModelView: View {
#ObservedObject var model: SimpleModel
Note: this in that direction - if you introduced ObservableObject then corresponding view should have ObservedObject wrapper to observe changes of that observable object's published properties.
In SimpleModelView, try changing:
#Binding var model: SimpleModel
to:
#ObservedObject var model: SimpleModel
#ObservedObjects provide binding values as well, and are required if you want state changes from classes conforming to ObservableObject

How to observe a TextField value with SwiftUI and Combine?

I'm trying to execute an action every time a textField's value is changed.
#Published var value: String = ""
var body: some View {
$value.sink { (val) in
print(val)
}
return TextField($value)
}
But I get below error.
Cannot convert value of type 'Published' to expected argument type 'Binding'
This should be a non-fragile way of doing it:
class MyData: ObservableObject {
var value: String = "" {
willSet(newValue) {
print(newValue)
}
}
}
struct ContentView: View {
#ObservedObject var data = MyData()
var body: some View {
TextField("Input:", text: $data.value)
}
}
In your code, $value is a publisher, while TextField requires a binding. While you can change from #Published to #State or even #Binding, that can't observe the event when the value is changed.
It seems like there is no way to observe a binding.
An alternative is to use ObservableObject to wrap your value type, then observe the publisher ($value).
class MyValue: ObservableObject {
#Published var value: String = ""
init() {
$value.sink { ... }
}
}
Then in your view, you have have the binding $viewModel.value.
struct ContentView: View {
#ObservedObject var viewModel = MyValue()
var body: some View {
TextField($viewModel.value)
}
}
I don't use combine for this. This it's working for me:
TextField("write your answer here...",
text: Binding(
get: {
return self.query
},
set: { (newValue) in
self.fetch(query: newValue) // any action you need
return self.query = newValue
}
)
)
I have to say it's not my idea, I read it in this blog: SwiftUI binding: A very simple trick
If you want to observe value then it should be a State
#State var value: String = ""
You can observe TextField value by using ways,
import SwiftUI
import Combine
struct ContentView: View {
#State private var Text1 = ""
#State private var Text2 = ""
#ObservedObject var viewModel = ObserveTextFieldValue()
var body: some View {
//MARK: TextField with Closures
TextField("Enter text1", text: $Text1){
editing in
print(editing)
}onCommit: {
print("Committed")
}
//MARK: .onChange Modifier
TextField("Enter text2", text: $Text2).onChange(of: Text2){
text in
print(text)
}
//MARK: ViewModel & Publisher(Combine)
TextField("Enter text3", text: $viewModel.value)
}
}
class ObserveTextFieldValue: ObservableObject {
#Published var value: String = ""
private var cancellables = Set<AnyCancellable>()
init() {
$value.sink(receiveValue: {
val in
print(val)
}).store(in: &cancellables)
}
}
#Published is one of the most useful property wrappers in SwiftUI, allowing us to create observable objects that automatically announce when changes occur that means whenever an object with a property marked #Published is changed, all views using that object will be reloaded to reflect those changes.
import SwiftUI
struct ContentView: View {
#ObservedObject var textfieldData = TextfieldData()
var body: some View {
TextField("Input:", text: $textfieldData.data)
}
}
class TextfieldData: ObservableObject{
#Published var data: String = ""
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Resources