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()
}
}
Related
I'm struggling with passing an variable to a class.
I have a class with different settings I store. The data is available and might be changed on different views.
class UserData: ObservableObject {
#Published var ZipCode = "DK6700"
}
The class is on my main view initialised as StateObject:
struct ContentView: View {
#StateObject var SavedData = UserData()
var body: some View {
NavigationView {
ChartView()
}
.edgesIgnoringSafeArea(.bottom)
.environmentObject(SavedData)
}
}
I call the Struct ChartView() where UserData is initialised as
#EnvironmentObject var SavedData: UserData
#ObservedObject var dataModel = DataModel()
and from the corresponding View, i can access the stored ZipCode.
So far so good.
The ChartView calls another class, where I download data in JSON format. This works as well, but I need the stored ZIP code in this class, and I can't figure out how to pass it.
Currently ZIP is hardcoded in the DataModel, and works, but it should be the stored value instead.
My DataModel():
class DataModel: ObservableObject {
#EnvironmentObject var SavedData: UserData
#MainActor #Published var Data: [MyData] = []
var ZIP:String = "3000"
#MainActor func reload() async {
let url = URL(string: "https://MyURL?zip=\(ZIP)")!
let urlSession = URLSession.shared
do {
...
} catch {
...
}
}
}
Any suggestions to get the stored ZIP code to my DataModel Class?
How about changing your function signature to
#MainActor func reload(for zipCode: String) async {
and passing it in when you call the function?
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 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.
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)
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.