Daniel Messner
Daniel Messner

Reputation: 2547

SwiftUI @State var initialization issue

I would like to initialise the value of a @State var in SwiftUI through the init() method of a Struct, so it can take the proper text from a prepared dictionary for manipulation purposes in a TextField. The source code looks like this:

struct StateFromOutside: View {
    let list = [
        "a": "Letter A",
        "b": "Letter B",
        // ...
    ]
    @State var fullText: String = ""

    init(letter: String) {
        self.fullText = list[letter]!
    }

    var body: some View {
        TextField($fullText)
    }
}

Unfortunately the execution fails with the error Thread 1: Fatal error: Accessing State<String> outside View.body

How can I resolve the situation? Thank you very much in advance!

Upvotes: 222

Views: 95231

Answers (9)

Nur_iOS
Nur_iOS

Reputation: 83

To initialize a @State variable in SwiftUI through the init method of a struct, you need to understand that @State properties are managed by SwiftUI and cannot be directly set within the initializer. Instead, you can use a workaround by utilizing a separate property for the initial value and then assigning it to the @State variable within the body of the view. Here is how you can achieve this:

struct StateFromOutside: View {
    let list = [
        "a": "Letter A",
        "b": "Letter B",
        // ...
    ]
    
    // Private property to hold the initial value
    private var initialText: String
    
    // @State variable to be used in the view
    @State private var fullText: String = ""
    
    init(letter: String) {
        self.initialText = list[letter] ?? ""
    }
    
    var body: some View {
        // Assign initial value to @State variable if it's not already set
        VStack {
            TextField("Enter text", text: $fullText)
                .onAppear {
                    if self.fullText.isEmpty {
                        self.fullText = self.initialText
                    }
                }
        }
    }
}

Explanation Private Property for Initial Value: A private property initialText is added to hold the initial value passed through the initializer. Setting the @State Variable: In the body of the view, use the onAppear modifier to set the @State variable fullText to initialText only if it hasn't been set already. This ensures that fullText gets its initial value when the view appears.

Upvotes: 0

Mojtaba Hosseini
Mojtaba Hosseini

Reputation: 119108

It's not an issue nowadays to set a default value of the @State variables inside the init method. But you MUST just get rid of the default value which you gave to the state and it will work as desired:

,,,
    @State var fullText: String // 👈 No default value should be here

    init(letter: String) {
        self.fullText = list[letter]!
    }

    var body: some View {
        TextField("", text: $fullText)
    }
}

Working demo: enter image description here

Upvotes: 3

RawMean
RawMean

Reputation: 8717

See the .id(count) in the example code below.

import SwiftUI
import MapKit

struct ContentView: View {
    @State private var count = 0
    
    var body: some View {
        Button("Tap me") {
            self.count += 1
            print(count)
        }
        Spacer()
        testView(count: count).id(count) // <------ THIS IS IMPORTANT. Without this "id" the initializer setting affects the testView only once and calling testView again won't change it (not desirable, of course)
    }
}



struct testView: View {
    var count2: Int
    @State private var region: MKCoordinateRegion
    
    init(count: Int) {
        count2 = 2*count
        print("in testView: \(count)")
        
        let lon =  -0.1246402 + Double(count) / 100.0
        let lat =  51.50007773 + Double(count) / 100.0
        let myRegion = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: lat, longitude: lon) , span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01))
        _region = State(initialValue: myRegion)
    }

    var body: some View {
        Map(coordinateRegion: $region, interactionModes: MapInteractionModes.all)
        Text("\(count2)")
    }
}

Upvotes: -3

Rad&#39;Val
Rad&#39;Val

Reputation: 9221

The top answer is an anti-pattern that will cause pain down the road, when the dependency changes (letter) and your state will not update accordingly.

One should never use State(initialValue:) or State(wrappedValue:) to initialize state in a View's init. In fact, State should only be initialized inline, like so:

@State private var fullText: String = "The value"

If that's not feasible, use @Binding, @ObservedObject, a combination between @Binding and @State or even a custom DynamicProperty

More about this here.

Upvotes: 27

Rodrigo Alvarez
Rodrigo Alvarez

Reputation: 78

Depending on the case, you can initialize the State in different ways:

// With default value

@State var fullText: String = "XXX"

// Not optional value and without default value

@State var fullText: String

init(x: String) {
    fullText = x
}

// Optional value and without default value

@State var fullText: String

init(x: String) {
    _fullText = State(initialValue: x)
}

Upvotes: 1

Rajat Dhasmana
Rajat Dhasmana

Reputation: 129

You can create a view model and initiate the same as well :

 class LetterViewModel: ObservableObject {

     var fullText: String
     let listTemp = [
         "a": "Letter A",
         "b": "Letter B",
         // ...
     ]

     init(initialLetter: String) {
         fullText = listTemp[initialLetter] ?? ""
     }
 }

 struct LetterView: View {

     @State var viewmodel: LetterViewModel

     var body: some View {
    
         TextField("Enter text", text: $viewmodel.fullText)
     }
 }

And then call the view like this:

 struct ContentView: View {

     var body: some View {

           LetterView(viewmodel: LetterViewModel(initialLetter: "a"))
     }
 }

By this you would also not have to call the State instantiate method.

Upvotes: 0

Kevin
Kevin

Reputation: 17536

SwiftUI doesn't allow you to change @State in the initializer but you can initialize it.

Remove the default value and use _fullText to set @State directly instead of going through the property wrapper accessor.

@State var fullText: String // No default value of ""

init(letter: String) {
    _fullText = State(initialValue: list[letter]!)
}

Upvotes: 729

Bogdan Farca
Bogdan Farca

Reputation: 4086

I would try to initialise it in onAppear.

struct StateFromOutside: View {
    let list = [
        "a": "Letter A",
        "b": "Letter B",
        // ...
    ]
    @State var fullText: String = ""

    var body: some View {
        TextField($fullText)
             .onAppear {
                 self.fullText = list[letter]!
             }
    }
}

Or, even better, use a model object (a BindableObject linked to your view) and do all the initialisation and business logic there. Your view will update to reflect the changes automatically.


Update: BindableObject is now called ObservableObject.

Upvotes: 50

bhargav K
bhargav K

Reputation: 64

The answer of Bogdan Farca is right for this case but we can't say this is the solution for the asked question because I found there is the issue with the Textfield in the asked question. Still we can use the init for the same code So look into the below code it shows the exact solution for asked question.

struct StateFromOutside: View {
    let list = [
        "a": "Letter A",
        "b": "Letter B",
        // ...
    ]
    @State var fullText: String = ""

    init(letter: String) {
        self.fullText = list[letter]!
    }

    var body: some View {
        VStack {
            Text("\(self.fullText)")
            TextField("Enter some text", text: $fullText)
        }
    }
}

And use this by simply calling inside your view

struct ContentView: View {
    var body: some View {
        StateFromOutside(letter: "a")
    }
}

Upvotes: 0

Related Questions