SwiftUI #State var initialization issue - swiftui

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

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.

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 DynamicPropertys work, here.

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)
}
}

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)
}

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

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.

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

Related

Left side of mutating operator isn't mutable: 'self' is immutable [duplicate]

Basically what i want to do is if you press the Button then entries should get a new CEntry. It would be nice if someone could help me out. Thanks!
struct AView: View {
var entries = [CEntries]()
var body: some View {
ZStack {
VStack {
Text("Hello")
ScrollView{
ForEach(entries) { entry in
VStack{
Text(entry.string1)
Text(entry.string2)
}
}
}
}
Button(action: {
self.entries.append(CEntries(string1: "he", string2: "lp")) <-- Error
}) {
someButtonStyle()
}
}
}
}
The Class CEntries
class CEntries: ObservableObject, Identifiable{
#Published var string1 = ""
#Published var string2 = ""
init(string1: String, string2: String) {
self.string1 = string1
self.string2 = string2
}
}
Views are immutable in SwiftUI. You can only mutate their state, which is done by changing the properties that have a #State property wrapper:
#State var entries: [CEntries] = []
However, while you could do that, in your case CEntries is a class - i.e. a reference type - so while you could detect changes in the array of entries - additions and removals of elements, you won't be able to detect changes in the elements themselves, for example when .string1 property is updated.
And it doesn't help that it's an ObservableObject.
Instead, change CEntries to be a struct - a value type, so that if it changes, the value itself will change:
struct CEntries: Identifiable {
var id: UUID = .init()
var string1 = ""
var string2 = ""
}
struct AView: View {
#State var entries = [CEntries]()
var body: some View {
VStack() {
ForEach(entries) { entry in
VStack {
Text(entry.string1)
Text(entry.string2)
}
}
Button(action: {
self.entries.append(CEntries(string1: "he", string2: "lp"))
}) {
someButtonStyle()
}
}
}
}

SwiftUI - Binding in ObservableObject

Let's say we have a parent view like:
struct ParentView: View {
#State var text: String = ""
var body: some View {
ChildView(text: $text)
}
}
Child view like:
struct ChildView: View {
#ObservedObject var childViewModel: ChildViewModel
init(text: Binding<String>) {
self.childViewModel = ChildViewModel(text: text)
}
var body: some View {
...
}
}
And a view model for the child view:
class ChildViewModel: ObservableObject {
#Published var value = false
#Binding var text: String
init(text: Binding<String>) {
self._text = text
}
...
}
Making changes on the String binding inside the child's view model makes the ChildView re-draw causing the viewModel to recreate itself and hence reset the #Published parameter to its default value. What is the best way to handle this in your opinion?
Cheers!
The best way is to use a custom struct as a single source of truth, and pass a binding into child views, e.g.
struct ChildViewConfig {
var value = false
var text: String = ""
// mutating funcs for logic
mutating func reset() {
text = ""
}
}
struct ParentView: View {
#State var config = ChildViewConfig()
var body: some View {
ChildView(config: $config)
}
}
struct ChildView: View {
#Binding var config: ChildViewConfig
var body: some View {
TextField("Text", text: $config.text)
...
Button("Reset") {
config.reset()
}
}
}
"ViewConfig can maintain invariants on its properties and be tested independently. And because ViewConfig is a value type, any change to a property of ViewConfig, like its text, is visible as a change to ViewConfig itself." [Data Essentials in SwiftUI WWDC 2020].

why can't I change the value of a var named coma after and if statement

I'd like to change the value of comma to true if a string contains a comma
xcode swfitui
struct ContentView: View {
#State var dos = ""
#State var coma : Bool = false
var body: some View {
Form {
TextField("dosis", text: $dos)
if dos.contains(",") == true {
coma = true
}
}
}
}
You cannot put that type code wherever you like in a View. The view expect to show other views, and your if is not returning a view. Try this approach where your if is in a modifier, or a function:
struct ContentView: View {
#State var dos = ""
#State var coma = false
var body: some View {
Form {
TextField("dosis", text: $dos)
.onChange(of: dos) { doz in
if doz.contains(",") {
coma = true
}
print("--> dos: \(dos) coma: \(coma)")
}
}
}
}
You can also use: .onSubmit {...}

swiftui cannot change #State value in sink

i am learning swiftui now and I am newbie for stackoverflow, I find a question,this is my code. I want to change the #State nopubName in sink ,but it's not work,the print is always "Nimar", I don't know why
struct ContentView: View {
#State var nopubName: String = "Nimar"
private var cancellable: AnyCancellable?
var stringSubject = PassthroughSubject<String, Never>()
init() {
cancellable = stringSubject.sink(receiveValue: handleValue(_:))
}
func handleValue(_ value: String) {
print("handleValue: '\(value)'")
self.nopubName = value
print("in sink "+nopubName)
}
var body: some View {
VStack {
Text(self.nopubName)
.font(.title).bold()
.foregroundColor(.red)
Spacer()
Button("sink"){
stringSubject.send("World")
print(nopubName)
}
}
}
}
You should only access a state property from inside the view’s body, or from methods called by it.
https://developer.apple.com/documentation/swiftui/state
You can get that functionality working in an ObservableObject and update an #Published To keep the UI updated
https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app
You don't need to use Combine, If you are within the View, you can change the value of #State variables directly
struct ContentView: View {
#State var nopubName: String = "Nimar"
var body: some View {
VStack {
Text(self.nopubName)
.font(.title).bold()
.foregroundColor(.red)
Spacer()
Button("sink"){
nopubName = "World"
}
}
}
}

Presenting a Sheet modally, #State value is not changing

I have tried much time on it but I couldn't figure out why it is not working.
The problem is when I tap the button, the new value inside the sheet is not updated. It always show the same value which is set up in start.
#State var value:String = "empty"
#State var explorePageIsEnabled:Bool = false
VStack{
Button("tap me"){
value = "the new one"
exploreStatusIsEnabled.toggle()
}
.sheet(isPresented: $exploreStatusIsEnabled, content: {
Text(value)
})
}
Deployment target is IOS 14+
Create a separate struct view for text and use Binding.
struct SheetView: View {
#Binding var value: String
var body: some View {
Text(value)
}
}
struct ContentView: View {
#State private var value: String = "empty"
#State private var explorePageIsEnabled: Bool = false
var body: some View {
VStack{
Button("tap me"){
value = "the new one"
explorePageIsEnabled.toggle()
}
.sheet(isPresented: $explorePageIsEnabled, content: {
SheetView(value: $value)
})
}
}
}