SwiftUI @State var initialization issue
SwiftuiSwiftui Problem Overview
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!
Swiftui Solutions
Solution 1 - Swiftui
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]!)
}
Solution 2 - Swiftui
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
.
Solution 3 - Swiftui
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")
}
}
Solution 4 - Swiftui
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 here
init(letter: String) {
self.fullText = list[letter]!
}
var body: some View {
TextField("", text: $fullText)
}
}
Solution 5 - Swiftui
The top answer is incorrect. 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
In your specific case, @Bindable
+ @State
+ onAppear
+ onChange
should do the trick.
More about this and in general how DynamicProperty
s work, here.
Solution 6 - Swiftui
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.
Solution 7 - Swiftui
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)")
}
}