How do I set an #State variable programmatically in SwiftUI - swiftui

I have a control that sets an #State variable to keep track of the selected tabs in a custom tab View. I can set the #State variable in code by setting the following:
#State var selectedTab: Int = 1
How do I set the initial value programmatically so that I can change the selected tab when the view is created?
I have tried the following:
1:
#State var selectedTab: Int = (parameter == true ? 1 : 2)
2:
init(default: Int) {
self.$selectedTab = default
}

Example of how I set initial state values in one of my views:
struct TodoListEdit: View {
var todoList: TodoList
#State var title = ""
#State var color = "None"
init(todoList: TodoList) {
self.todoList = todoList
self._title = State(initialValue: todoList.title ?? "")
self._color = State(initialValue: todoList.color ?? "None")
}

Joe Groff: "#State variables in SwiftUI should not be initialized from data you pass down through the initializer. The correct thing to do is to set your initial state values inline:"
#State var selectedTab: Int = 1
You should use a Binding to provide access to state that isn't local to your view.
#Binding var selectedTab: Int
Then you can initialise it from init and you can still pass it to child views.
Source: https://forums.swift.org/t/state-messing-with-initializer-flow/25276/3

This was so simple to solve once I discovered the answer!
You simply remove the initial value from the initialization by changing the declaration from:
#State var selectedTab: Int = 1
to:
#State var selectedTab: Int
and then the selectedTab variable automatically becomes a parameter in the instantiation statement. So the initialization would be:
TabBarContentView(selectedTab: 2)
Its that simple!!!!!

Related

How would I go about storing individual variables from a passed in #Binding variable?

My #Binding weight variable connects to my Source of Truth further up in my code. But I also need to let my user edit this with a TextField(). So, I am trying to create a local variable of type String because TextField requires type Bindable.
Perhaps I'm approaching this wrong.
struct SetsBar: View {
#Binding var weight: Int
#Binding var reps: Int
#State var weightString: String = String(weight)
init(weight: Binding<Int>, reps: Binding<Int>) {
self._weight = weight
self._reps = reps
}
var body: some View {
HStack {
TextField("\(weight)", text: $weightString)
}
}
}
I get an error on my #State property
Cannot use instance member 'weight' within property initializer; property initializers run before 'self' is available
You can bind weight directly using TextField variant with formatter (configuring formatter as much as needed, below is simplified variant for demo), like
var body: some View {
HStack {
TextField("\(weight)", value: $weight, formatter: NumberFormatter())
}
}

Updating slider value with Swift UI

I'm trying to pass data from one view to another and update the slider.
I understand you can do this with #State and #Binding property wrappers however it does not appear to be updating.
On my content view I have:
#State var startNumber: Double = 0
#State var endNumber: Double = 5000
Slider(value: $startNumber, in: 0...10000, step: 0.1)
On my second view I have:
#Binding var startNumber: Double
#Binding var endNumber: Double
#State var number: String = ""
#State var number2: String = ""
FirstResponderTextField(text: $number2, placeholder: "\(endNumber)")
Button(action: {
startNumber = Double(number) ?? 0
endNumber = Double(number2) ?? 0
print("\(startNumber)")
print("\(number)")
print("\(endNumber)")
print("\(number2)")
self.presentationMode.wrappedValue.dismiss()
}
I can see that the value doesn't update the #Binding as it stays at 0.
So I can't even get the variable to update, would someone know why?
Then I understand for the value to show on the content view that I would need to call onAppear after the Hstack for it to show the updated value?
HStack {
}onAppear {
self.startNumber = $startNumber
}
However, this doesn't seem to work either?

SwiftUI EnvironmentObject not available in View initializer?

I passed the environmentObject appSettings into my view successfully. I can use it to modify my font and the picker in my View. But if I try to access an environmentObject published variable in the view init() it crashes with:
Thread 1: Fatal error: No ObservableObject of type AppSettings found.
A View.environmentObject(_:) for AppSettings may be missing as an ancestor of this view.
Are there special rules about using an environmentObject in a custom SwiftUI View initializer?
Here's the start of my view code. The environmentObject is appSettings. If I comment out line 2 in my initializer and uncomment line 3 the app works. Note that I use "appSettings.interfaces" successfully later in my Picker.
struct CaptureFilterView: View {
#State var etherCapture: EtherCapture? = nil
#EnvironmentObject var appSettings: AppSettings
#Binding var frames: [Frame]
#State var captureFilter: String = ""
#State var error: String = ""
#State var numberPackets = 10
#State var interface: String = ""
init(frames: Binding<[Frame]>) {
self._frames = frames
self.interface = appSettings.interfaces.first ?? "en0" //CRASH HERE
//self.interface = "en0" //uncomment this and comment line above to make app "work"
}
var body: some View {
HStack() {
...
Picker(selection: $interface, label: Text("")) {
ForEach(appSettings.interfaces, id: \.self) { interfaceName in
Text(interfaceName).tag(interfaceName)
}
}
Here's where I create my top-level content view in my AppDelegate.swift
let contentView = ContentView(showCapture: true).environmentObject(appSettings)
And just to be sure I also pass on the environmentObject when creating my CaptureFilterView in my top level ContentView. This is not necessary and does not change the behavior.
if showCapture { CaptureFilterView(frames: self.$frames).environmentObject(appSettings) }
For reference here is the top of my appSettings:
class AppSettings: ObservableObject {
#Published var font: Font
#Published var interfaces: [String]
SwiftUI EnvironmentObject not available in View initializer?
Yes, SwiftUI EnvironmentObject not available in View initializer. Why? It is simple - it is injected after object initialiazation.
Let's consider how it is done on example of above ContentView:
let contentView = ContentView(showCapture: true).environmentObject(appSettings)
so what's going on here? Here
instantiation & initialisation of value for the type ContentView
let newInstance = ContentView.init(showCapture: true)
calling function func environmentObject() on newInstance injected appSetting property
let contentView = newInstance.environmentObject(appSettings)

SwiftUI - Updating #State when Global changes

I'd like to update an UI element on an overview view when data on another view is changed.
I looked into #EnvironmentalObject and #Binding. However, an update to either object does not appear to force a view reload. Only changes to #State force renders.
Also, in the case described below, the ChangeView is not a child of OverviewView. Therefore #Binding is not an option anyway.
Data.swift
struct ExampleData : Hashable {
var id: UUID
var name: String
}
var globalVar: ExampleData = ExampleData(id: UUID(), name:"")
OverviewView.swift
struct OverviewView: View {
#State private var data: ExampleData = globalVar
var body: some View {
Text(data.name)
}
}
ChangeView.swift
struct ChangeView: View {
#State private var data: ExampleData = globalVar
var body: some View {
TextField("Name", text: $data.name, onEditingChanged: { _ in
globalVar = data }, onCommit: { globalVar = data })
}
}
Changes within the ChangeView TextField will update the globalVar. However, this will not update the Text on the OverviewView when switching back to the view.
I am aware that using global variables is "ugly" coding. How do I handle data that will be used in a multitude of unrelated views?
Please advise on how to better handle such a situation.
OverviewView and ChangeView hold different copies of the ExampleData struct in their data variables (When assigning a struct to another variable, you're effectively copying it instead of referencing it like an object.) so changing one won't affect the other.
#EnvironmentObject suits your requirements.
Here's an example:
Since, we're using #EnvironmentObject, you need to either convert ExampleData to
a class, or use a class to store it. I'll use the latter.
class ExampleDataHolder: ObservableObject {
#Published var data: ExampleData = ExampleData(id: UUID(), name:"")
}
struct CommonAncestorOfTheViews: View {
var body: some View {
CommonAncestorView()
.environmentObject(ExampleDataHolder())
}
}
struct OverviewView: View {
#EnvironmentObject var dataHolder: ExampleDataHolder
var body: some View {
Text(dataHolder.data.name)
}
}
struct ChangeView: View {
#EnvironmentObject var dataHolder: ExampleDataHolder
var body: some View {
TextField("Name", text: $dataHolder.data.name)
}
}

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