95
votes

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!

4
Use State(initialValue:) - onmyway133
@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. - Tul
The most upvoted answer is probably the answer you want in preference to the accepted answer. - Benjohn

4 Answers

34
votes

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.

389
votes

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]!)
}
0
votes

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")
    }
}
-3
votes

See the .id(count) in the example come 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)")
}
}