Reputation: 11975
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
Upvotes: 2
Views: 2527
Reputation: 386008
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") }
}
Upvotes: 6