How to observe a TextField value with SwiftUI and Combine? - ios

I'm trying to execute an action every time a textField's value is changed.
#Published var value: String = ""
var body: some View {
$value.sink { (val) in
print(val)
}
return TextField($value)
}
But I get below error.
Cannot convert value of type 'Published' to expected argument type 'Binding'

This should be a non-fragile way of doing it:
class MyData: ObservableObject {
var value: String = "" {
willSet(newValue) {
print(newValue)
}
}
}
struct ContentView: View {
#ObservedObject var data = MyData()
var body: some View {
TextField("Input:", text: $data.value)
}
}

In your code, $value is a publisher, while TextField requires a binding. While you can change from #Published to #State or even #Binding, that can't observe the event when the value is changed.
It seems like there is no way to observe a binding.
An alternative is to use ObservableObject to wrap your value type, then observe the publisher ($value).
class MyValue: ObservableObject {
#Published var value: String = ""
init() {
$value.sink { ... }
}
}
Then in your view, you have have the binding $viewModel.value.
struct ContentView: View {
#ObservedObject var viewModel = MyValue()
var body: some View {
TextField($viewModel.value)
}
}

I don't use combine for this. This it's working for me:
TextField("write your answer here...",
text: Binding(
get: {
return self.query
},
set: { (newValue) in
self.fetch(query: newValue) // any action you need
return self.query = newValue
}
)
)
I have to say it's not my idea, I read it in this blog: SwiftUI binding: A very simple trick

If you want to observe value then it should be a State
#State var value: String = ""

You can observe TextField value by using ways,
import SwiftUI
import Combine
struct ContentView: View {
#State private var Text1 = ""
#State private var Text2 = ""
#ObservedObject var viewModel = ObserveTextFieldValue()
var body: some View {
//MARK: TextField with Closures
TextField("Enter text1", text: $Text1){
editing in
print(editing)
}onCommit: {
print("Committed")
}
//MARK: .onChange Modifier
TextField("Enter text2", text: $Text2).onChange(of: Text2){
text in
print(text)
}
//MARK: ViewModel & Publisher(Combine)
TextField("Enter text3", text: $viewModel.value)
}
}
class ObserveTextFieldValue: ObservableObject {
#Published var value: String = ""
private var cancellables = Set<AnyCancellable>()
init() {
$value.sink(receiveValue: {
val in
print(val)
}).store(in: &cancellables)
}
}

#Published is one of the most useful property wrappers in SwiftUI, allowing us to create observable objects that automatically announce when changes occur that means whenever an object with a property marked #Published is changed, all views using that object will be reloaded to reflect those changes.
import SwiftUI
struct ContentView: View {
#ObservedObject var textfieldData = TextfieldData()
var body: some View {
TextField("Input:", text: $textfieldData.data)
}
}
class TextfieldData: ObservableObject{
#Published var data: String = ""
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Related

Pass #Published var by reference to function from View

I am working on a registration screen which has a ViewModel attached to it.
The ViewModel is just a class which extends my registration screen class, where I place all of my business logic.
extension RegistrationScreen {
#MainActor class ViewModel : ObservableObject {
//business logic goes here
}
}
The ViewModel has #Published variables to represent two states for every text field in the screen: text and validationText.
#Published var first: String = ""
#Published var firstValidationMessage: String = ""
within that ViewModel I have a call to a helper function from another class which checks to see if the field's text is empty and if so it can set the fields validation text to an error, that looks like this:
class FieldValidation: Identifiable {
func isFieldEmptyAndSetError(fieldText:String, fieldValidationText:Binding<String>) -> Bool {
if(fieldText.isEmpty){
fieldValidationText.wrappedValue = "Required"
return true
}else{
fieldValidationText.wrappedValue = ""
return false
}
}
}
to call that function from my viewModel I do the following:
FieldValidation().isFieldEmptyAndSetError(fieldText:first,fieldValidationText: firstValidationMessage)
This is throwing runtime errors which are hard to decrypt for me since I am new to Xcode and iOS in general.
My question is, how can I get this passing by reference to work? alternatively if the way I am doing is not possible please explain what is going on.
It's kind of weird having a function that receives a #Published as a parameter, if you want to handle all the validations in the viewModel, this seems as an easy solution for Combine. I would suggest you to create an extension to validate if the string is empty, since .isEmpty does not trim the text.
The class
class FieldValidation: Identifiable {
static func isFieldEmptyAndSetError(fieldText:String) -> String {
return fieldText.isEmpty ? "Required" : ""
}
}
The view Model
import Foundation
import Combine
class viewModel: ObservableObject {
#Published var text1 = ""
#Published var text2 = ""
#Published var text3 = ""
#Published var validationText1 = ""
#Published var validationText2 = ""
#Published var validationText3 = ""
init() {
self.$text1.map{newText in FieldValidation.isFieldEmptyAndSetError(fieldText: newText)}.assign(to: &$validationText1)
self.$text2.map{newText in FieldValidation.isFieldEmptyAndSetError(fieldText: newText)}.assign(to: &$validationText2)
}
func validateText(value: String) {
self.validationText3 = FieldValidation.isFieldEmptyAndSetError(fieldText: value)
}
}
The view would look like this:
import SwiftUI
struct ViewDate: View {
#StateObject var myviewModel = viewModel()
var body: some View {
VStack{
Text("Fill the next fields:")
TextField("", text: $myviewModel.text1)
.border(Color.red)
Text(myviewModel.validationText1)
TextField("", text: $myviewModel.text2)
.border(Color.blue)
Text(myviewModel.validationText2)
TextField("", text: $myviewModel.text3)
.border(Color.green)
.onChange(of: myviewModel.text3) { newValue in
if(newValue.isEmpty){
self.myviewModel.validateText(value: newValue)
}
}
Text(myviewModel.validationText3)
}.padding()
}
}

Custom property wrapper not accurately reflecting the state of my TextField in SwiftUI, any idea why?

I've created a property wrapper that I want to insert some logic into and the "set" value is doing the right thing, but the textfield isn't updating with all uppercase text. Shouldn't the text field be showing all uppercase text or am I misunderstanding how this is working?
Also this is a contrived example, my end goal is to insert a lot more logic into a property wrapper, I'm just using the uppercase example to get it working. I've searched all over the internet and haven't found a working solution.
import SwiftUI
import Combine
struct ContentView: View {
#StateObject var vm = FormDataViewModel()
var body: some View {
Form {
TextField("Name", text: $vm.name)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
class FormDataViewModel: ObservableObject {
#Capitalized var name: String = ""
}
#propertyWrapper
public class Capitalized {
#Published var value: String
public var wrappedValue: String {
get { value }
set { value = newValue.uppercased() } //Printing this shows all caps
}
public var projectedValue: AnyPublisher<String, Never> {
return $value
.eraseToAnyPublisher()
}
public init(wrappedValue: String) {
value = wrappedValue
}
}
SwiftUI watches #Published properties in #StateObject or #ObservedObject and triggers UI update on changes of them.
But it does not go deep inside the ObservableObject. Your FormDataViewModel does not have any #Published properties.
One thing you can do is that simulating what #Published will do on value changes.
class FormDataViewModel: ObservableObject {
#Capitalized var name: String = ""
private var nameObserver: AnyCancellable?
init() {
nameObserver = _name.$value.sink {_ in
self.objectWillChange.send()
}
}
}
Please try.
This can be done with standard #Published that seems simpler and more reliable.
Here is a solution. Tested with Xcode 12 / iOS 14.
class FormDataViewModel: ObservableObject {
#Published var name: String = "" {
didSet {
let capitalized = name.uppercased()
if name != capitalized {
name = capitalized
objectWillChange.send()
}
}
}
}

Array of binding variables for multiple toggles in MVVM pattern in SwiftUI

I have a simple use case of having a VStack of a dynamic number of Text with Toggle buttons coming from an array.
import SwiftUI
public struct Test: View {
#ObservedObject public var viewModel = TestViewModel()
public init() {
}
public var body: some View {
VStack {
ForEach(viewModel.models) { model in
ToggleView(title: <#T##Binding<String>#>, switchState: <#T##Binding<Bool>#>)
//how to add the above
}
}.padding(50)
}
}
struct ToggleView: View {
#Binding var title: String
#Binding var switchState: Bool
var body: some View {
VStack {
Toggle(isOn: $switchState) {
Text(title)
}
}
}
}
public class TestModel: Identifiable {
#Published var state: Bool {
didSet {
//do something
//publish to the view that the state has changed
}
}
#Published var title: String
init(state: Bool, title: String) {
self.state = state
self.title = title
}
}
public class TestViewModel: ObservableObject {
#Published var models: [TestModel] = [TestModel(state: false, title: "Title 1"), TestModel(state: true, title: "Title 2")]
}
The following questions arise:
In MVVM pattern, is it ok to have the binding variables in model class or should it be inside the view model?
How to send the message of state change from model class to view/scene when the toggle state is changed?
If using an array of binding variables in view model for each of the toggle states, how to know which particular element of array has changed? (see the following code snippet)
class ViewModel {
#Published var dataModel: [TestModel]
#Published var toggleStates = [Bool]() {
didSet {
//do something based on which element of the toggle states array has changed
}
}
}
Please help with the above questions.
here is one way you could achieve what you desire.
Has you will notice you have to use the binding power of #ObservedObject.
The trick is to use indexes to reach the array elements for you binding.
If you loop on the array elements model directly you loose the underlying binding properties.
struct Test: View {
#ObservedObject public var viewModel = TestViewModel()
var body: some View {
VStack {
ForEach(viewModel.models.indices) { index in
ToggleView(title: self.viewModel.models[index].title, switchState: self.$viewModel.models[index].state)
}
}.padding(50)
}
}
class TestViewModel: ObservableObject {
#Published var models: [TestModel] = [
TestModel(state: false, title: "Title 1"),
TestModel(state: true, title: "Title 2")]
}
struct ToggleView: View {
var title: String
#Binding var switchState: Bool
var body: some View {
VStack {
Toggle(isOn: $switchState) {
Text(title)
}
}
}
}
class TestModel: Identifiable {
var state: Bool
var title: String
init(state: Bool, title: String) {
self.title = title
self.state = state
}
}
Hope this does the trick for you.
Best

How to extract String value from Observed Object in Swift

I want to extract String value from Observed Object
This is example code
import SwiftUI
import Combine
class SetViewModel : ObservableObject {
private static let userDefaultTextKey = "textKey"
#Published var text: String = UserDefaults.standard.string(forKey: SetViewModel.userDefaultTextKey) ?? ""
private var canc: AnyCancellable!
init() {
canc = $text.debounce(for: 0.2, scheduler: DispatchQueue.main).sink { newText in
UserDefaults.standard.set(newText, forKey: SetViewModel.userDefaultTextKey)
}
}
deinit {
canc.cancel()
}
}
struct SettingView: View {
#ObservedObject var viewModel = SettingViewModel()
var body: some View {
ZStack {
Rectangle().foregroundColor(Color.white).edgesIgnoringSafeArea(.all).background(Color.white)
VStack {
TextField("test", text: $viewModel.text).textFieldStyle(BottomLineTextFieldStyle()).foregroundColor(.red)
Text($viewModel.text) //I want to get String Value from $viewModel.text
}
}
}
}
I want to use "$viewModel.text"'s String value. How can I do this?
Here is fix
Text(viewModel.text) // << use directly, no $ needed, it is for binding
try this:
struct SettingView: View {
#ObservedObject var viewModel = SetViewModel()
var body: some View {
ZStack {
Rectangle().foregroundColor(Color.white).edgesIgnoringSafeArea(.all).background(Color.white)
VStack {
TextField("test", text: self.$viewModel.text)
.textFieldStyle(PlainTextFieldStyle())
.foregroundColor(.red)
Text(viewModel.text) //I want to get String Value from $viewModel.text
}
}
}
}

Multiple SwiftUI views sharing data members

Here's my situation.... I have a view called SwiftUIView() - it contains a picker and a textbox
You can see it here:
import SwiftUI
import Combine
class section_array: ObservableObject {
let objectWillChange = ObservableObjectPublisher()
var first_value = 0 { willSet { objectWillChange.send() } }
var second_value = 0 { willSet { objectWillChange.send() } }}
struct SwiftUIView: View {
#Binding var master_sections: [ContentView.my_Section]
#ObservedObject private var first_value: section_array = section_array()
// #Binding var current_section: ContentView.my_Section
#State private var comment: String = String()
var body: some View {
VStack {
Picker(selection: self.$first_value.first_value , label: Text("Subsection")) {
ForEach(0 ..< self.master_sections.count) {
Text(self.master_sections[$0].name)
}}
TextField("Enter your comment.", text: $comment)
}
}
}
Attempting to utlizie this view here:
ForEach(master_subsections) { result in
SwiftUIView(master_sections: self.$subsections)
}
Causes the right amount of SwiftUIViews to appear, however, they all seem to share the same $comment and $first_value, these are private fields - I need them unique to each occurance of the SwiftUIView().

Resources