2

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

6
  • You are accessing the value before initialization, but even if you had no issue that would bad idea doing things in initialization, you can use on appear Commented Sep 6, 2021 at 9:06
  • thanks a lot for the prompt answer! what do you mean with onAppear? where should I do that, in the JobsTab or in the JobsViewModel? Commented Sep 6, 2021 at 9:08
  • I prefer the pattern where you inject the ViewModelError from the super view. You should probably inject a JobsTabViewModel which you have initialised with a JobsModel and a CategoriesModel or something; this view shouldn't know anything about other views viewmodels Commented Sep 6, 2021 at 9:28
  • Using a custom init as you are does not work well in SwiftUI. Stay away from custom inits. Your Viewmodels are likely leaking and your state variables are not being initialized properly. developer.apple.com/documentation/swiftui/… Commented Sep 6, 2021 at 10:46
  • 1
    @Cristik sorry about that; I've added it now to the original question Commented Sep 6, 2021 at 13:46

1 Answer 1

7

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") }
}
Sign up to request clarification or add additional context in comments.

1 Comment

With giving the right answer for wrong method, he would be confused!

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.