I tried to change an #State Variable with this code (from an other struct, not the struct where I declared the Variable):
MyExampleStruct().myVariable = "New Value"
But it dont worked. I dont know how I can change the value of them, has anybody an idea?
Thanks Boothosh
You aren't going to manipulate the #State of a view directly like that. However, there are plenty of options that you can use to achieve similar results depending on your use case.
Option 1 : Binding
Use when you need to manipulate the variable from both parent and child:
struct ParentView : View {
#State var myVariable : String = "Test"
var body: some View {
VStack {
MyExampleStruct(myVariable: $myVariable)
Button("Change") {
myVariable = "\(Date())"
}
}
}
}
struct MyExampleStruct : View {
#Binding var myVariable : String
var body : some View {
VStack {
Text(myVariable)
Button("Change") {
myVariable = "\(Date())"
}
}
}
}
Option 2: ObservableObject
Use when the state needs to be manipulated in multiple places, passed as a reference via property or via envronmentObject (https://www.hackingwithswift.com/quick-start/swiftui/how-to-use-environmentobject-to-share-data-between-views)
class StateManager : ObservableObject {
#Published var myVariable : String = "Test"
}
struct ParentView : View {
#StateObject var state = StateManager() //use #ObservedObject if targeting iOS 13 or macOS 10.15
var body: some View {
VStack {
MyExampleStruct(state: state)
Button("Change") {
state.myVariable = "\(Date())"
}
}
}
}
struct MyExampleStruct : View {
#ObservedObject var state : StateManager
var body : some View {
Text(state.myVariable)
}
}
Option 3: No state
Use when only the parent needs to manipulate the value:
struct ParentView : View {
#State var myVariable : String = "Test"
var body: some View {
VStack {
MyExampleStruct(myVariable: myVariable)
Button("Change") {
myVariable = "\(Date())"
}
}
}
}
struct MyExampleStruct : View {
var myVariable : String
var body : some View {
Text(myVariable)
}
}
You need to pass it as a Binding to the other view.
For example
struct SubView: View
{
#Binding var myVariable: String
init(_ s: Binding<String>) {
self._myVariable = s
}
var body: some View {
Text(myVariable).onTapGuesture { myVariable = "New Value" }
}
}
struct ContentView: View
{
#State myVariable: String = "Some String"
var body {
SubView($myVariable)
}
}
Of course, this specific example is pretty lame because you hardly need a subview to change a Text string, but you can imagine if you had several views that used myVariable, one of them could change the value, and the others could be responsive to it. It's not the only way, or necessarily even the best way to achieve this in every circumstance. Sometimes you'd want an ObservableObject or an #EnvironmentObject, or with SwiftUI 2, #StateObject.
Related
I would like to use a picker to select a value and store that value in a variable contained in another class. How can I do that? Here is my code:
my contentview
struct ContentView: View {
#State public var selectedOperator = ""
#ObservedObject var bitwiseCalculator = BitwiseCalculator()
let operators = ["AND", "OR", "XOR"]
var body: some View {
VStack {
Picker("Operator", selection: $selectedOperator) {
ForEach(operators, id: \.self) { op in
Text(op)
}
}
}
}
}
and the variable in the class where I want to save it
class BitwiseCalculator: ObservableObject {
#Published var bitwiseOperator = ""
}
Use
$bitwiseCalculator.bitwiseOperator
& switch your
#ObservedObject to #StateObject
Using a TextField on a mac app, when I hit 'return' it resets to its original value, even if the underlying binding value is changed.
import SwiftUI
class ViewModel {
let defaultS = "Default String"
var s = ""
var sBinding: Binding<String> {
.init(get: {
print("Getting binding \(self.s)")
return self.s.count > 0 ? self.s : self.defaultS
}, set: {
print("Setting binding")
self.s = $0
})
}
}
struct ContentView: View {
#State private var vm = ViewModel()
var body: some View {
TextField("S:", text: vm.sBinding)
.padding()
}
}
Why is this? Shouldn't it 'get' the binding value and use that? (i.e. shouldn't I see my print statement "Getting binding" in the console after I hit 'return' on the textfield?).
Here you go!
class ViewModel: ObservableObject {
#Published var s = "Default String"
}
struct ContentView: View {
#StateObject private var vm = ViewModel()
var body: some View {
TextField("S:", text: $vm.s)
.padding()
}
}
For use in multiple views, in every view where you'd like to use the model add:
#EnvironmentObject private var vm: ViewModel
But don't forget to inject the model to the main view:
ContentView().environmentObject(ViewModel())
The following code produces the runtime error: #EnvironmentObject error: may be missing as an ancestor of this view. The tState in the environment is an #ObservedObject.
struct TEditorView: View {
#EnvironmentObject private var tState: TState
#State var name = ""
init() {
self._name = State(initialValue: tState.name)
}
var body: some View {
...
}
}
XCode 12.0.1
iOS 14
The answer is that an Environment Object apparently cannot be accessed in an init() function. However, an ObservedObject can be. So I changed the code to this and it works. To make it easy I turned TState into a singleton that I could access anywhere. This could probably replace the use of #EnvironmentObject in many situations.
struct TEditorView: View {
#ObservedObject private var tState = TState.shared
//#EnvironmentObject private var tState: TState
#State var name = ""
init() {
self._name = State(initialValue: tState.name)
}
var body: some View {
...
}
}
A different approach here could be to inject the initial TState value in the constructor and do-away with the #EnvironmentObject completely. Then from the parent view you can use the #EnvironmentObject value when creating the view.
struct TEditorView: View {
#State var name = ""
init(tState: TState) {
self._name = State(initialValue: tState.name)
}
var body: some View {
...
}
}
struct ContentView: View {
#EnvironmentObject private var tState: TState
var body: some View {
TEditorView(state: tState)
}
}
Or use a #Binding instead of #State if the name value is meant to be two-way.
In general I'd also question why you need the #EnvironmentObject in the constructor. The idea is with a #EnvironmentObject is that it's represented the same in all views, so you should only need it body.
If you need any data transformations it should be done in the object model itself, not the view.
The #State should be set as private and per the documentation should only be accessed in the View body.
https://developer.apple.com/documentation/swiftui/state
An #EnvironmentObject should be set using the ContentView().environmentObject(YourObservableObject)
https://developer.apple.com/documentation/combine/observableobject
https://developer.apple.com/documentation/swiftui/stateobject
Below is some Sample code
import SwiftUI
class SampleOO: ObservableObject {
#Published var name: String = "init name"
}
//ParentView
struct OOSample: View {
//The first version of an #EnvironmentObject is an #ObservedObject or #StateObject
//https://developer.apple.com/tutorials/swiftui/handling-user-input
#ObservedObject var sampleOO: SampleOO = SampleOO()
var body: some View {
VStack{
Button("change-name", action: {
self.sampleOO.name = "OOSample"
})
Text("OOSample = " + sampleOO.name)
//Doing this should fix your error code with no other workarounds
ChildEO().environmentObject(sampleOO)
SimpleChild(name: sampleOO.name)
}
}
}
//Can Display and Change name
struct ChildEO: View {
#EnvironmentObject var sampleOO: SampleOO
var body: some View {
VStack{
//Can change name
Button("ChildEO change-name", action: {
self.sampleOO.name = "ChildEO"
})
Text("ChildEO = " + sampleOO.name)
}
}
}
//Can only display name
struct SimpleChild: View {
var name: String
var body: some View {
VStack{
//Cannot change name
Button("SimpleChild - change-name", action: {
print("Can't change name")
//self.name = "SimpleChild"
})
Text("SimpleChild = " + name)
}
}
}
struct OOSample_Previews: PreviewProvider {
static var previews: some View {
OOSample()
}
}
Aim:
I have a model which is an ObservableObject. It has a Bool property, I would like to use this Bool property to initialise a #Binding variable.
Questions:
How to convert an #ObservableObject to a #Binding ?
Is creating a #State the only way to initialise a #Binding ?
Note:
I do understand I can make use of #ObservedObject / #EnvironmentObject, and I see it's usefulness, but I am not sure a simple button needs to have access to the entire model.
Or is my understanding incorrect ?
Code:
import SwiftUI
import Combine
import SwiftUI
import PlaygroundSupport
class Car : ObservableObject {
#Published var isReadyForSale = true
}
struct SaleButton : View {
#Binding var isOn : Bool
var body: some View {
Button(action: {
self.isOn.toggle()
}) {
Text(isOn ? "On" : "Off")
}
}
}
let car = Car()
//How to convert an ObservableObject to a Binding
//Is creating an ObservedObject or EnvironmentObject the only way to handle a Observable Object ?
let button = SaleButton(isOn: car.isReadyForSale) //Throws a compilation error and rightly so, but how to pass it as a Binding variable ?
PlaygroundPage.current.setLiveView(button)
Binding variables can be created in the following ways:
#State variable's projected value provides a Binding<Value>
#ObservedObject variable's projected value provides a wrapper from which you can get the Binding<Subject> for all of it's properties
Point 2 applies to #EnvironmentObject as well.
You can create a Binding variable by passing closures for getter and setter as shown below:
let button = SaleButton(isOn: .init(get: { car.isReadyForSale },
set: { car.isReadyForSale = $0} ))
Note:
As #nayem has pointed out you need #State / #ObservedObject / #EnvironmentObject / #StateObject (added in SwiftUI 2.0) in the view for SwiftUI to detect changes automatically.
Projected values can be accessed conveniently by using $ prefix.
You have several options to observe the ObservableObject. If you want to be in sync with the state of the object, it's inevitable to observe the state of the stateful object. From the options, the most commons are:
#State
#ObservedObject
#EnvironmentObject
It is upto you, which one suits your use case.
No. But you need to have an object which can be observed of any change made to that object in any point in time.
In reality, you will have something like this:
class Car: ObservableObject {
#Published var isReadyForSale = true
}
struct ContentView: View {
// It's upto you whether you want to have other type
// such as #State or #ObservedObject
#EnvironmentObject var car: Car
var body: some View {
SaleButton(isOn: $car.isReadyForSale)
}
}
struct SaleButton: View {
#Binding var isOn: Bool
var body: some View {
Button(action: {
self.isOn.toggle()
}) {
Text(isOn ? "Off" : "On")
}
}
}
If you are ready for the #EnvironmentObject you will initialize your view with:
let contentView = ContentView().environmentObject(Car())
struct ContentView: View {
#EnvironmentObject var car: Car
var body: some View {
SaleButton(isOn: self.$car.isReadyForSale)
}
}
class Car: ObservableObject {
#Published var isReadyForSale = true
}
struct SaleButton: View {
#Binding var isOn: Bool
var body: some View {
Button(action: {
self.isOn.toggle()
}) {
Text(isOn ? "On" : "Off")
}
}
}
Ensure you have the following in your SceneDelegate:
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView()
.environmentObject(Car())
In my case i used .constant(viewModel) to pass viewModel to ListView #Binding var viewModel
Example
struct CoursesView: View {
#StateObject var viewModel = CoursesViewModel()
var body: some View {
ZStack {
ListView(viewModel: .constant(viewModel))
ProgressView().opacity(viewModel.isShowing)
}
}
}
struct ListView: View {
#Binding var viewModel: CoursesViewModel
var body: some View {
List {
ForEach(viewModel.courses, id: \.id) { course in
Text(couse.imageUrl)
}
}
}
}
Normally we can use didSet in swift to monitor the updates of a variable. But it didn't work for a #Binding variable. For example, I have the following code:
#Binding var text {
didSet {
......
}
}
But the didSet is never been called.Any idea? Thanks.
Instead of didSet you can always use onReceive (iOS 13+) or onChange (iOS 14+):
import Combine
import SwiftUI
struct ContentView: View {
#State private var counter = 1
var body: some View {
ChildView(counter: $counter)
Button("Increment") {
counter += 1
}
}
}
struct ChildView: View {
#Binding var counter: Int
var body: some View {
Text(String(counter))
.onReceive(Just(counter)) { value in
print("onReceive: \(value)")
}
.onChange(of: counter) { value in
print("onChange: \(value)")
}
}
}
You shouldn’t need a didSet observer on a #Binding.
If you want a didSet because you want to compute something else for display when text changes, just compute it. For example, if you want to display the count of characters in text:
struct ContentView: View {
#Binding var text: String
var count: Int { text.count }
var body: some View {
VStack {
Text(text)
Text(“count: \(count)”)
}
}
}
If you want to observe text because you want to make some other change to your data model, then observing the change from your View is wrong. You should be observing the change from elsewhere in your model, or in a controller object, not from your View. Remember that your View is a value type, not a reference type. SwiftUI creates it when needed, and might store multiple copies of it, or no copies at all.
The best way is to wrap the property in an ObservableObject:
final class TextStore: ObservableObject {
#Published var text: String = "" {
didSet { ... }
}
}
And then use that ObservableObject's property as a binding variable in your view:
struct ContentView: View {
#ObservedObject var store = TextStore()
var body: some View {
TextField("", text: $store.text)
}
}
didSet will now be called whenever text changes.
Alternatively, you could create a sort of makeshift Binding value:
TextField("", text: Binding<String>(
get: {
return self.text
},
set: { newValue in
self.text = newValue
...
}
))
Just note that with this second strategy, the get function will be called every time the view is updated. I wouldn't recommend using this approach, but nevertheless it's good to be aware of it.