SwiftUI ViewModel published property and binding - ios

My question is probably the result of a misunderstanding but I can't figure it out, so here it is:
When using a component like a TextField or any other component requiring a binding as input
TextField(title: StringProtocol, text: Binding<String>)
And a View with a ViewModel, I naturally thought that I could simply pass my ViewModel #Published properties as binding :
class MyViewModel: ObservableObject {
#Published var title: String
#Published var text: String
}
// Now in my view
var body: some View {
TextField(title: myViewModel.title, text: myViewModel.$text)
}
But I obviously can't since the publisher cannot act as binding. From my understanding, only a #State property can act like that but shouldn't all the #State properties live only in the View and not in the view model? Or could I do something like that :
class MyViewModel: ObservableObject {
#Published var title: String
#State var text: String
}
And if I can't, how can I transfer the information to my ViewModel when my text is updated?

You were almost there. You just have to replace myViewModel.$text with $myViewModel.text.
class MyViewModel: ObservableObject {
var title: String = "SwiftUI"
#Published var text: String = ""
}
struct TextFieldView: View {
#StateObject var myViewModel: MyViewModel = MyViewModel()
var body: some View {
TextField(myViewModel.title, text: $myViewModel.text)
}
}
TextField expects a Binding (for text parameter) and StateObject property wrapper takes care of creating bindings to MyViewModel's properties using dynamic member lookup.

Related

Pass #Published var by reference to function from View

I am working on a registration screen which has a ViewModel attached to it.
The ViewModel is just a class which extends my registration screen class, where I place all of my business logic.
extension RegistrationScreen {
#MainActor class ViewModel : ObservableObject {
//business logic goes here
}
}
The ViewModel has #Published variables to represent two states for every text field in the screen: text and validationText.
#Published var first: String = ""
#Published var firstValidationMessage: String = ""
within that ViewModel I have a call to a helper function from another class which checks to see if the field's text is empty and if so it can set the fields validation text to an error, that looks like this:
class FieldValidation: Identifiable {
func isFieldEmptyAndSetError(fieldText:String, fieldValidationText:Binding<String>) -> Bool {
if(fieldText.isEmpty){
fieldValidationText.wrappedValue = "Required"
return true
}else{
fieldValidationText.wrappedValue = ""
return false
}
}
}
to call that function from my viewModel I do the following:
FieldValidation().isFieldEmptyAndSetError(fieldText:first,fieldValidationText: firstValidationMessage)
This is throwing runtime errors which are hard to decrypt for me since I am new to Xcode and iOS in general.
My question is, how can I get this passing by reference to work? alternatively if the way I am doing is not possible please explain what is going on.
It's kind of weird having a function that receives a #Published as a parameter, if you want to handle all the validations in the viewModel, this seems as an easy solution for Combine. I would suggest you to create an extension to validate if the string is empty, since .isEmpty does not trim the text.
The class
class FieldValidation: Identifiable {
static func isFieldEmptyAndSetError(fieldText:String) -> String {
return fieldText.isEmpty ? "Required" : ""
}
}
The view Model
import Foundation
import Combine
class viewModel: ObservableObject {
#Published var text1 = ""
#Published var text2 = ""
#Published var text3 = ""
#Published var validationText1 = ""
#Published var validationText2 = ""
#Published var validationText3 = ""
init() {
self.$text1.map{newText in FieldValidation.isFieldEmptyAndSetError(fieldText: newText)}.assign(to: &$validationText1)
self.$text2.map{newText in FieldValidation.isFieldEmptyAndSetError(fieldText: newText)}.assign(to: &$validationText2)
}
func validateText(value: String) {
self.validationText3 = FieldValidation.isFieldEmptyAndSetError(fieldText: value)
}
}
The view would look like this:
import SwiftUI
struct ViewDate: View {
#StateObject var myviewModel = viewModel()
var body: some View {
VStack{
Text("Fill the next fields:")
TextField("", text: $myviewModel.text1)
.border(Color.red)
Text(myviewModel.validationText1)
TextField("", text: $myviewModel.text2)
.border(Color.blue)
Text(myviewModel.validationText2)
TextField("", text: $myviewModel.text3)
.border(Color.green)
.onChange(of: myviewModel.text3) { newValue in
if(newValue.isEmpty){
self.myviewModel.validateText(value: newValue)
}
}
Text(myviewModel.validationText3)
}.padding()
}
}

SwiftUI: Using different property wrappers for the same variable

in iOS13 I do the following to bind my View to my model:
class MyModel: ObservableObject {
#Published var someVar: String = "initial value"
}
struct MyView: View {
#ObservedObject var model = MyModel()
var body: some View {
Text("the value is \(model.someVar)")
}
}
in iOS14 there is a new property wrapper called #StateObject that I can use in the place of #ObservedObject, I need this snippet of code to be compatible with iOS13 and iOS14 while leveraging iOS14's new feature, how can I do that with #StateObject for the same variable ?
Different property wrappers generate different types of hidden properties, so you cannot just conditionally replace them. Here is a demo of possible approach.
Tested with Xcode 12 / iOS 14 (deployment target 13.6)
struct ContentView: View {
var body: some View {
if #available(iOS 14, *) {
MyNewView()
} else {
MyView()
}
}
}
class MyModel: ObservableObject {
#Published var someVar: String = "initial value"
}
#available(iOS, introduced: 13, obsoleted: 14, renamed: "MyNewView")
struct MyView: View {
#ObservedObject var model = MyModel()
var body: some View {
CommonView().environmentObject(model)
}
}
#available(iOS 14, *)
struct MyNewView: View {
#StateObject var model = MyModel()
var body: some View {
CommonView().environmentObject(model)
}
}
struct CommonView: View {
#EnvironmentObject var model: MyModel
var body: some View {
Text("the value is \(model.someVar)")
}
}
ObservableObject and #Published are part of the Combine framework and you should only use those when you require a Combine pipeline to assign the output to the #Published var. What you should be using for your data is #State use it as follows:
struct MyView: View {
#State var text = "initial value"
var body: some View {
VStack{
Text("the value is \(text)")
TextField("", text: $text)
}
}
}
If you have multiple vars or need functions then you should refactor these into their own struct. Multiple related properties in their own struct makes the View more readable, can maintain invariance on its properties and be tested independently. And because the struct is a value type, any change to a property, is visible as a change to the struct (Learn this in WWDC 2020 Data Essentials in SwiftUI ). Implement as follows:
struct MyViewConfig {
var text1 = "initial value"
var text2 = "initial value"
mutating func reset(){
text1 = "initial value"
text2 = "initial value"
}
}
struct MyView: View {
#Binding var config: MyViewConfig
var body: some View {
VStack{
Text("the value is \(config.text1)")
TextField("", text: $config.text1)
Button("Reset", action: reset)
}
}
func reset() {
config.reset()
}
}
struct ContentView {
#State var config = MyViewConfig()
var body: some View {
MyView(config:$config)
}
}
SwiftUI is designed to take advantage of value semantics where all the data is in structs which makes it run super fast. If you unnecessarily create objects then you are slowing it all down.
To answer the question, that use of ObservableObject to init is in correct. Every time the View struct is init a new object will also be init which will slow down SwiftUI. You’ll need to either use a global var or singleton to store the objects and use onAppear to init and onDissapear to destroy. In the WWDC video where StateObject is introduced you’ll hear him say “you don’t need to mess with onDissapear anymore” so that is your clue on how to simulate StateObject. By the way, it is totally fine to update your app with new features only available in the new OS, users that have not yet updated their OS will just stay on the old version of the app and is a much simpler way to work.

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

SwiftUI and Combine what is the difference between Published and 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.

Resources