PresentationMode triggers "Variable X used before being initialized"

Viewed 408

Error messages I'm trying to write a reusable quick select field editor for a SwiftUI application. The code below only shows the error Variable 'self.fieldValue' used before initialized if the @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode> is present. If I remove that, the view compiles fine. Anyone know why the presentationmode environment variable is causing this and how to fix it? I need the presentationmode env var so I can quickly pop the view once the user clicks on a button.

@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>

@State var viewName = "Edit Field"
@Binding var fieldValue: Int
@State var fieldName: String = ""
var options: [Int]

init(title: String, field: String, value: Binding<Int>, list: [Int]) {
    viewName = title
    fieldName = field
    _fieldValue = value
    options = list
}

For full view code, see below.

import SwiftUI

struct CommonNumberEditView: View {
    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
    
    @State var viewName = "Edit Field"
    @Binding var fieldValue: Int
    @State var fieldName: String = ""
    var options: [Int]
    
    init(title: String, field: String, value: Binding<Int>, list: [Int]) {
        viewName = title
        fieldName = field
        _fieldValue = value
        options = list
    }
    
    var body: some View {
        ZStack {
            Image("sky")
                .resizable()
                .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
                .edgesIgnoringSafeArea(.all)
                .opacity(0.5)
            
            VStack(alignment: .center, spacing: 5)
            {
                JumpLogNumberField(label: self.fieldName, val: self.$fieldValue)
                
                ScrollView {
                    
                    VStack(alignment: .center, spacing: 5) {
                        ForEach(options, id: \.self) { item in
                            Group {
                                Button(action: {
                                    self.presentationMode.wrappedValue.dismiss()
                                }) {
                                    Text(String(item))
                                        .foregroundColor(.black)
                                        .padding(20)
                                }
                                .frame(minWidth:0, maxWidth: .infinity)
                                .background(Color.white.opacity(0.6))
                            }
                        }
                    }
                }
                .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .top)
            }
            .padding(.top, 40)
            .padding(.leading, 40)
            .padding(.trailing, 40)
            .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
            
        }.navigationBarTitle(viewName, displayMode: .inline)
    }
}

struct CommonFieldEditView_Previews: PreviewProvider {
    
    static var previews: some View {
        CommonNumberEditView(title: "Edit Field", field: "Altitude", value: .constant(5000), list: [ 13500, 10000, 5500, 3500 ])
    }
}
2 Answers

Here is fixed part of code:

struct CommonNumberEditView: View {
    @Environment(\.presentationMode) var presentationMode

    @State private var viewName: String
    @Binding var fieldValue: Int
    @State private var fieldName: String
    var options: [Int]

    init(title: String, field: String, value: Binding<Int>, list: [Int]) {
        _viewName = State(initialValue: title)
        _fieldName = State(initialValue: field)
        _fieldValue = value
        options = list
    }

// ...

Tested with Xcode 13 / iOS 15

Warning

You should not initialize State variables in your View's init because that can cause problems with how SwiftUI works and updates.

By creating a new State variable each time a new instance of your struct is created you don't get the benefit of @State sharing the value across all instances of your struct when the parent view is re-rendered/updates.

For example if you are putting those @State values in TextFields every time the parent view is reloaded/updated (e.g. because the @Binding changes) it will create a new subview that resets them to the initial values.


You can initialize a @Binding in init because the value is coming from somewhere else so it doesn't matter if it gets overwritten, it will have the same source of truth, and thus same value.

Solution

In your view you aren't changing the values of viewName or fieldName so you don't need to use @State. You can just remove @State from viewName and fieldName, and everything should still work as you expect, just with a cleaner init.

If you do need them to be editable you should convert them into @Binding, just like the fieldValue is so that they update whatever is actually storing the value.

Related