noloman
noloman

Reputation: 11975

Struct initialization in SwiftUI: 'self' used before all stored properties are initialized

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

Answers (1)

rob mayoff
rob mayoff

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

Related Questions