SwiftUI TextField resets value and ignores binding - swift

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

Related

How to assign a value selected from a Picker to a variable in another class?

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

SwiftUI - How to change/access #Published var value via toggle from View?

I'm making a simple password generation app. The idea was simple.
There are 2 toggles in the View that are bound to ObservableObject class two #Published bool vars. The class should return a different complexity of generated password to a new View dependently on published vars true/false status after clicking generate button.
Toggles indeed change published var status to true/false (when I print it on toggle) and the destination view does show the password for false/false combination but for some reason, after clicking generate, they always stay false unless I manually change their value to true. Can toggles change permanently the value of #Published var values somehow?
I can't seem to find a suitable workaround. Any solutions how to make this work?
MainView
import SwiftUI
struct MainView: View {
#ObservedObject var manager = PasswordManager()
var body: some View {
NavigationView() {
VStack {
ZStack {
Toggle(isOn: $manager.includeNumbers) {
Text("Include numbers")
.italic()
}
}
ZStack {
Toggle(isOn: $manager.includeCharacters) {
Text("Include special characters")
.italic()
}
}
NavigationLink(destination: PasswordView(), label: {
Text("Generate")
})
}
.padding(80)
}
}
PasswordManager
import Foundation
class PasswordManager: ObservableObject {
#Published var includeNumbers = false
#Published var includeCharacters = false
let letters = ["A", "B", "C", "D", "E"]
let numbers = ["1", "2", "3", "4", "5"]
let specialCharacters = ["!", "#", "#", "$", "%"]
var password: String = ""
func generatePassword() -> String {
password = ""
if includeNumbers == false && includeCharacters == false {
for _ in 1...5 {
password += letters.randomElement()!
}
}
else if includeNumbers && includeCharacters {
for _ in 1...3 {
password += letters.randomElement()!
password += numbers.randomElement()!
password += specialCharacters.randomElement()!
}
}
return password
}
}
View that shows password
import SwiftUI
struct PasswordView: View {
#ObservedObject var manager = PasswordManager()
var body: some View {
Text(manager.generatePassword())
}
}
The problem is caused by the fact that your PasswordView creates its own PasswordManager. Instead, you need to inject it from the parent view.
You should never initialise an #ObservedObject inside the View itself, since whenever the #ObservedObject's objectWillChange emits a value, it will reload the view and hence create a new object. You either need to inject the #ObservedObject or declare it as #StateObject if you are targeting iOS 14.
PasswordView needs to have PasswordManager injected from MainView, since they need to use the same instance to have shared state. In MainView, you can use #StateObject if targeting iOS 14, otherwise you should inject PasswordManager even there.
import SwiftUI
struct PasswordView: View {
#ObservedObject private var manager: PasswordManager
init(manager: PasswordManager) {
self.manager = manager
}
var body: some View {
Text(manager.generatePassword())
}
}
struct MainView: View {
#StateObject private var manager = PasswordManager()
var body: some View {
NavigationView() {
VStack {
ZStack {
Toggle(isOn: $manager.includeNumbers) {
Text("Include numbers")
.italic()
}
}
ZStack {
Toggle(isOn: $manager.includeCharacters) {
Text("Include special characters")
.italic()
}
}
NavigationLink(destination: PasswordView(manager: manager), label: {
Text("Generate")
})
}
.padding(80)
}
}
}

SwiftUI #EnvironmentObject error: may be missing as an ancestor of this view -- accessing object in the init()

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

SwiftUI #Binding value can not change and called init

I want to make a picker use SwiftUI, when I change the value in ChildView, it will not change and called ChildView init.
class ViewModel: ObservableObject {
#Published var value: Int
init(v: Int) {
self.value = v
}
}
struct ChildView: View {
#Binding var value: Int
#ObservedObject var vm = ViewModel(v: 0)
init(value: Binding<Int>) {
self._value = value
print("ChildView init")
}
var body: some View {
VStack {
Text("param value: \(value)")
Text("#ObservedObject bar: \(vm.value)")
Button("(child) bar.value++") {
self.vm.value += 1
}
}
.onReceive(vm.$value) { value = $0 }
}
}
struct ContentView: View {
#State var value = 0
var body: some View {
VStack {
Text("(parent) \(self.value)")
ChildView(value: $value)
}
}
}
But when I remove Text("(parent) \(self.value)") in ContentView, it seems to be normal.
This happens because anytime ChildView gets init-ialized - which happens when ContentView's body is recomputed - it create a new instance of ViewModel with the value 0.
Determine first who "owns" the data. If it's some external object, like ViewModel, then it should get instantiated somewhere where an instance could be longer-lived, for example in ContentView (but it would depend on your real use case):
struct ContentView: View {
#State var value = 0
var childVm = ViewModel(v: 0)
var body: some View {
VStack {
Text("(parent) \(self.value)")
ChildView(vm: childVm, value: $value)
}
}
}
struct ChildView: View {
#Binding var value: Int
#ObservedObject var vm: ViewModel
init(vm: ViewModel, value: Binding<Int>) {
self._value = value
self.vm = vm
print("ChildView init")
}
// ...
}
In general, the described behavior is expected, because source of truth for value is in parent, and updating it via binding you update all places where it is used. That result in rebuild parent body, so recreate child view.
SwiftUI 2.0
Solution is simple - use state object
struct ChildView: View {
#Binding var value: Int
#StateObject var vm = ViewModel(v: 0) // << here !!
// ... other code
SwiftUI 1.0+
Initialize view model with updated bound value
struct ChildView: View {
#Binding var value: Int
#ObservedObject var vm: ViewModel // << declare !!
init(value: Binding<Int>) {
self._value = value
self.vm = ViewModel(v: value.wrappedValue) // << initialize !!
// .. other code

Binding value from an ObservableObject

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