How can I convert this variable from my class into the #State variable in my View? I am getting an exception right now:
Cannot assign value of type 'Binding' to type 'String'
Inside my View:
struct ContentView: View {
//Error private members
#State var alert = false
#State var error = ""
#EnvironmentObject var manager: HttpAuth
func verify(){
//Cannot assign value of type Binding<String> to type String
self.error = $manager.error
}
}
Inside my HttpAuth class
class HttpAuth: ObservableObject{
//Error private members
var alert = false
var error: String = ""
..
}
You don't need binding here, #State is a property wrapper, so assign like property
func verify(){
self.error = manager.error // no $
}
Note: actually you can use manager.error directly, w/o remapping it to provide state, just make it #Published
class HttpAuth: ObservableObject{
//Error private members
var alert = false
#Published var error: String = ""
the only caution(!) you must modify this published error only on main queue, and then all usage inside View will be updated automatically.
Related
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()
}
}
I'm trying to pass a Binding to my VM which is supposed to be a filter so the VM fetches objects according to the filtering passed by params.
Unfortunately, I'm not able to initialize the VM, as I'm getting the error 'self' used before all stored properties are initialized in the line where I'm initializing my VM self.jobsViewModel = JobsViewModel(jobFilter: $jobFilter)
struct JobsTab: View {
#ObservedObject var jobsViewModel: JobsViewModel
#ObservedObject var categoriesViewModel: CategoriesViewModel
#StateObject var searchText: SearchText = SearchText()
#State private var isEditing: Bool
#State private var showFilter: Bool
#State private var jobFilter: JobFilter
init() {
self.categoriesViewModel = CategoriesViewModel()
self.jobFilter = JobFilter(category: nil)
self.showFilter = false
self.isEditing = false
self.jobsViewModel = JobsViewModel(jobFilter: $jobFilter)
}
I think I'm initializing all the vars, and self.searchText isn't in the init block because the compiler complains that it is a get-only property.
Is there any other way to do this?
Thanks!
EDIT: Here's my VM:
class JobsViewModel: ObservableObject {
#Published var isLoading: Bool = false
#Published var jobs: [Jobs] = []
#Binding var jobFilter: JobFilter
init(jobFilter: Binding<JobFilter>) {
_jobFilter = jobFilter
}
...
}
struct JobFilter {
var category: Category?
}
My idea was to have the job filter as a state in the JobsTab, and every time that state changes, the VM would try to fetch the jobs that match the JobFilter
You shouldn't create #ObservedObject values in your initializer. Doing so leads to bugs, because you'll create new instances every time the view is recreated. Either jobsViewModel and categoriesViewModel should be passed as arguments to init, or you should be using #StateObject for those properties.
But anyway, you actually asked: why can't we use $jobFilter before initializing jobsViewModel?
Let's start by simplifying the example:
struct JobsTab: View {
#State var jobFilter: String
var jobFilterBinding: Binding<String>
init() {
jobFilter = ""
jobFilterBinding = $jobFilter
// ^ 🛑 'self' used before all stored properties are initialized
}
var body: some View { Text("hello") }
}
So, what's going on here? It'll help if we “de-sugar” the use of #State. The compiler transforms the declaration of jobFilter into three properties:
struct JobsTab: View {
private var _jobFilter: State<String>
var jobFilter: String {
get { _jobFilter.wrappedValue }
nonmutating set { _jobFilter.wrappedValue = newValue }
}
var $jobFilter: Binding<String> {
get { _jobFilter.projectedValue }
}
var jobFilterBinding: Binding<String>
init() {
_jobFilter = State<String>(wrappedValue: "")
jobFilterBinding = $jobFilter
// ^ 🛑 'self' used before all stored properties are initialized
}
var body: some View { Text("hello") }
}
Notice now that $jobFilter is not a stored property. It is a computed property. So accessing $jobFilter means calling its “getter”, which is a method on self. But we cannot call a method on self until self is fully initialized. That's why we get an error if we try to use $jobFilter before initializing all stored properties.
The fix is to avoid using $jobFilter. Instead, we can use _jobFilter.projectedValue directly:
struct JobsTab: View {
#State var jobFilter: String
var jobFilterBinding: Binding<String>
init() {
jobFilter = ""
jobFilterBinding = _jobFilter.projectedValue
}
var body: some View { Text("hello") }
}
I'm using a DataStore environmentObject to pass data around my app, however I'm at a point where I needed to create a specific View Model class for a view, but I need to be able to pass the instance of my dataStore to my WorkoutDetailViewModel but can't figure out a way to do it. I tried passing it in as a parameter to WorkoutDetailViewModel, but the compiler complains Cannot use instance member 'dataStore' within property initializer; property initializers run before 'self' is available. How else can I do this?
struct NewWorkoutDetailView: View {
#EnvironmentObject var dataStore: DataStore
#StateObject var workoutDetailViewModel = WorkoutDetailViewModel(dataStore: dataStore)
var body: some View {
}
}
final class WorkoutDetailViewModel: ObservableObject {
var dataStore: DataStore
#Published var fullyLoadedWorkout: TrackerWorkout?
#Published var notes: String = ""
#Published var workoutImage = UIImage()
#Published var workoutLocation: String = ""
public func editWorkout() {
dataStore.loadWorkouts()
}
}
I have a class method that apparently doesn't see the #EnvironmentObject var categories: Categories object at the top of the class. I know that this works since I use it in several other files. It also means that my coding in SceneDelegate is correct. The software is crashing with the error: Thread 1: Fatal error: No ObservableObject of type Categories found. A View.environmentObject(_:) for Categories may be missing as an ancestor of this view. The error is occurring in the method updateTotals() in the "for" loop
struct CatItem: Codable, Identifiable {
var id = UUID()
var catNum: Int
var catName: String <-- this is the class I'm trying to reference
var catTotal: Double
var catPix: String
var catShow: Bool
}
class Categories: ObservableObject {
#Published var catItem: [CatItem]
}
class BaseCurrency: ObservableObject {
#EnvironmentObject var userData: UserData
#EnvironmentObject var categories: Categories
var foundNew: Bool = false
var newRate: Double = 0.0
var baseCur: BaseModel
//-----------------------------------------------------
// new base currency so need to update the system totals
//-----------------------------------------------------
func updateTotals() -> () {
for index in 0...(categories.catItem.count - 1) { <-- error here
categories.catItem[index].catTotal *= self.newRate
}
userData.totalArray[grandTotal] *= self.newRate
userData.totalArray[transTotal] *= self.newRate
userData.totalArray[userTotal] *= self.newRate
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(self.baseCur) {
UserDefaults.standard.set(encoded, forKey: "base")
}
self.foundNew = false
}
}
I was reading somewhere recently that #EnvironmentObject is just like #State in that a change in either parameter will cause body to update the view. Therefore neither of these should be in a class. I have reorganized my software and have not seen the error since.
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)