SwiftUI @State var initialization issue
Asked Answered
F

8

216

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!

Famine answered 20/6, 2019 at 18:7 Comment(4)
Use State(initialValue:)Mc
@Daniel please make the answer with 150+ coming on second number as accepted answer. As like me, many missed the second answer and stay stuck for a lot of time.Penelopepeneplain
The most upvoted answer is probably the answer you want in preference to the accepted answer.Eleaseeleatic
Please don't use the most upvoted answer! Why it is not a good idea to set it in an initializer is clearly explained in other answers - It harms view identity and is almost never the right solution. Please don't just take whatever from StackOverflow for granted and use it in your production code.Pageantry
A
49

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.

Afterimage answered 20/6, 2019 at 19:0 Comment(4)
Thank you, this works perfectly (although I have applied the .onAppear on a VStack which is around the TextField).Famine
Hacking around a little longer I have noticed that your solution is a workaround for this very situation, but I ran into more situations where I have to initialise a @State var. Although it would be possible to apply this method there as well I do not think that this is the very best way to handle it.Famine
@DanielMessner : Yes, I agree, for more complicated cases just use a model object (BindableObject) and do the initialization in that object, either when running init() or triggered in the view by an .onAppear event.Afterimage
This is not considered the best solution for the particular case in the question - but it is the best solution for another type of problem. I have a @State whose initial value depends on another object I pass in, so each time I show the view, I want it to have a different initial value. The other answer does not solve that, but setting the state in onAppear does.Syllabogram
P
708

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]!)
}
Pest answered 27/9, 2019 at 15:4 Comment(16)
This should be the accepted answer, onAppear may be too late in some cases.Beacon
Where is the underscore documented?Tonie
docs.swift.org/swift-book/LanguageGuide/Properties.html#ID617Pest
Work like a dream! I was searching the way to change @State value when initializing for few days, in my case I need to init PageView from the main view. Thus, where could we know that SwiftUI doesn't allow us to change @State? Because I hope I could figure this out it by my own next time.Cairngorm
I don't recall seeing State(...) having been brought to light elsewhere - for me, this and _xxx was a perfect combination enabling things like initializing @State variables used with Slider. Great answer.Camellia
@Tonie At first I was hesitant to use this because I thought it was accessing some internal API, but it turns out it's part of the Swift language. For concise information about how property wrappers work under the hood including the synthesized underscore (_), see docs.swift.org/swift-book/ReferenceManual/Attributes.html under propertyWrapperRightminded
You should also note that the state you initialise inside init will probably immediately be overridden after all the stored properties are initialised. See this comment where an  engineer explicitly says not to initialise @State using init but to do it inline instead.Hexavalent
I am passing a struct into the view with the @State variable. the struct has a parameter called page which is a Int. So lets say the struct that im passing is called Work, I initialise it with var work: Work and I have a State variable defined as @State var count: Int. How could I initialise count with work.page?Githens
This solution is good, but it's not complete. the problem is that subsequent calls to the view have no effect on the State variable. The solution is easy: add ".id(some_var)" to the caller in the parent view. See my answer for complete exampleThirtieth
The answer is not a solution to the problem. It will not work "like a dream", rather nightmarish. Here, we need to use bindings which can be initialised within the initialiser, if we want to mutate the value within the view. Initialising @State in init will do it only the very first time a value of this view will be created. Subsequent views will not see the value which is passed as argument in the init, but the previous value which is cached by the SwiftUI system.Psalmist
developer.apple.com/forums/thread/657393 "init is not a good place to update the value of @State vars."Raynold
Came across this post because my issue was the state variable was always nil in the initializer. Thanks this answer.Phonate
I'm new to SwiftUI, but found my way to this (helpful, if not perfect, reading the comments) answer while trying to find ways to generate PreviewProvider views with pre-determined/mock state that forced my application to display a certain way.Suspend
Subsequent inits will ignore changes to _fullText. initialValue is only mean to be used with non-dynamic values.Faulkner
developer.apple.com/documentation/swiftui/state Note that the official docs state pretty clearly: "Always initialize state by providing a default value in the state’s declaration." Of course, it wouldn't be SwiftUI documentation if they explained why.Cyrie
This is not a correct answer and shouldn't beChayachayote
A
49

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.

Afterimage answered 20/6, 2019 at 19:0 Comment(4)
Thank you, this works perfectly (although I have applied the .onAppear on a VStack which is around the TextField).Famine
Hacking around a little longer I have noticed that your solution is a workaround for this very situation, but I ran into more situations where I have to initialise a @State var. Although it would be possible to apply this method there as well I do not think that this is the very best way to handle it.Famine
@DanielMessner : Yes, I agree, for more complicated cases just use a model object (BindableObject) and do the initialization in that object, either when running init() or triggered in the view by an .onAppear event.Afterimage
This is not considered the best solution for the particular case in the question - but it is the best solution for another type of problem. I have a @State whose initial value depends on another object I pass in, so each time I show the view, I want it to have a different initial value. The other answer does not solve that, but setting the state in onAppear does.Syllabogram
T
22

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.

Trigonal answered 24/2, 2022 at 4:14 Comment(1)
Thanks for writing the article. This answer should be upvoted more so that people realize they shouldn't set the state variables in initializer at all.Pageantry
P
1

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

Picrate answered 12/2, 2022 at 13:13 Comment(2)
With this solution I get the error "Variable 'self.toggleValue' used before being initialized"Blinders
@Blinders this code does not even have self.toggleValue. Just use the demo code and adapt yours, step by step to see what you are missing.Picrate
D
1

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)
}
Destructible answered 10/10, 2022 at 2:44 Comment(0)
M
0

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")
    }
}
Meredithmeredithe answered 4/3, 2020 at 13:22 Comment(0)
S
0

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.

Skirt answered 5/3, 2022 at 20:33 Comment(0)
T
-3

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)")
    }
}
Thirtieth answered 12/12, 2020 at 3:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.