How to detect what changed a #Published value in SwiftUI? - ios

I have an ObservableObject with a #Published value, how can I detect if the value was changed via TextField view or it was set directly (When Button is tapped for instance)?
struct ContentView: View {
#ObservedObject var model = Model()
var body: some View {
VStack {
Button("set value") {
self.model.value = "user set value"
}
TextField("value", text: $model.value)
}
}
}
class Model: ObservableObject {
#Published var value = ""
var anyCancellable: AnyCancellable?
init() {
anyCancellable = $value.sink { val in
// if changed by Button then ...
// if changed by TextField then ...
}
}
}
My real case scenario sounds like this: when the user changes the value a request have to be sent to the server with the new value, but the server can also respond with a new value (in that case a new request to server should not be sent), so I have to distinguish between the case when the user changes the value (via TextField) and the case when the server changes the value.

You can do this with #Binding easily. I don't think you need to use #Published
If you still want to use it you can try this code
struct ContentView: View {
#ObservedObject var model = Model()
var body: some View {
VStack {
Button("set value") {
DispatchQueue.main.async{
self.model.value = "user set value"
}
}
TextField("value", text: $model.value)
}
}
}

You can pass an onEditingChanged closure to the TextField initializer. You should use the closure to trigger the server request.
struct ContentView: View {
#ObservedObject var model = Model()
var body: some View {
VStack {
Button("set value") {
self.model.value = "user set value"
}
TextField(
"value",
text: $model.value,
onEditingChanged: { _ in self.model.sendServerRequest() )
}
}
}

Related

Supply any string value to swiftUI form in different for textfield

I have two view file. I have textfield. I want to supply string value to the textfield from another view
File 1 :- Place where form is created
struct ContentView: View {
#State var subjectLine: String = ""
var body: some View {
form {
Section(header: Text(NSLocalizedString("subjectLine", comment: ""))) {
TextField("SubjectLine", text: $subjectLine
}
}
}
}
File 2 :- Place where I want to provide value to the string and that will show in the textfield UI
struct CalenderView: View {
#Binding var subjectLine: String
var body : some View {
Button(action: {
subjectLine = "Assigned default value"
}, label: {
Text("Fill textfield")
}
}
})
}
}
This is not working. Any other way we can supply value to the textfield in other view file.
As i can understand you have binding in CalenderView
that means you want to navigate there when you navigate update there.
struct ContentView: View {
#State private var subjectLine: String = ""
#State private var showingSheet: Bool = false
var body: some View {
NavigationView {
Form {
Section(header: Text(NSLocalizedString("subjectLine", comment: ""))) {
TextField("SubjectLine", text: $subjectLine)
}
}
.navigationBarItems(trailing: nextButton)
.sheet(isPresented: $showingSheet) {
CalenderView(subjectLine: $subjectLine)
}
}
}
var nextButton: some View {
Button("Next") {
showingSheet.toggle()
}
}
}
CalendarView
struct CalenderView: View {
#Binding var subjectLine: String
#Environment(\.dismiss) private var dismiss
var body: some View {
Button {
subjectLine = "Assigned default value"
dismiss()
} label: {
Text("Fill textfield")
}
}
}

SwiftUI: Why onReceive run duplicate when binding a field inside ObservableObject?

This is my code and "print("run to onReceive (text)")" run twice when text change (like a image). Why? and thank you!
import SwiftUI
class ContentViewViewModel : ObservableObject {
#Published var text = ""
}
struct ContentView: View {
#StateObject private var viewModel = ContentViewViewModel()
var body: some View {
ZStack {
TextField("pla", text: $viewModel.text)
.padding()
}
.onReceive(viewModel.$text) { text in
print("run to onReceive \(text)")
}
}
}
I think it's because the view is automatically updated as your #Published property in your ViewModel changes and the .onReceive modifier updates the view yet again due to the 2 way binding created by viewModel.$text resulting in the view being updated twice each time.
If you want to print the text as it changes you can use the .onChange modifier instead.
class ContentViewViewModel: ObservableObject {
#Published var text = ""
}
struct ContentView: View {
#StateObject private var viewModel = ContentViewViewModel()
var body: some View {
ZStack {
TextField("pla", text: $viewModel.text)
.padding()
}.onChange(of: viewModel.text) { newValue in
print("run to onChange \(newValue)")
}
}
}
onChanged in SwiftUI
Because you have a #Published variable inside the #StateObject of your view, the changes in that variable will automatically update the view.
If you add the .onReceive() method, you will:
update the view because you have the #Published var
update it again when the .onReceive() method listens to the change
Just delete the .onReceive() completely and it will work:
class ContentViewViewModel : ObservableObject {
#Published var text = ""
}
struct ContentView: View {
#StateObject private var viewModel = ContentViewViewModel()
var body: some View {
ZStack {
TextField("pla", text: $viewModel.text)
.padding()
}
// It still works without this modifier
//.onReceive(viewModel.$text) { text in
// print("run to onReceive \(text)")
//}
}
}

SwiftUI TextField onChange not triggered

New to SwiftUI and trying to figure something out.
I'm implementing a barcode scanner, which consists of a TextField, Button and CameraView.
CameraView will scan a barcode, display the serial number in the TextField, and the Button will use the serial number to pair a bluetooth device.
So now in my SwiftUI class, I have my UI setup like so:
struct BTLEConnectionView: View {
#ObservedObject var bluetoothConnectorViewModel: BluetoothConnectorViewModel
#ObservedObject var barcodeScannerViewModel = BarcodeScannerViewModel()
#Binding var deviceID: String
#State private var serialNumber: String = ""
var body: some View {
VStack {
HStack {
DeviceIDTextField(deviceID: $deviceID, serialNumber: $serialNumber, viewModel: barcodeScannerViewModel)
PairButton(bluetoothConnectorViewModel: bluetoothConnectorViewModel, barcodeScannerViewModel: barcodeScannerViewModel, serialNumber: $serialNumber)
}
.padding()
BarcodeScannerView(barscannerViewModel: barcodeScannerViewModel)
}
}
}
struct DeviceIDTextField: View {
#Binding var deviceID: String
#Binding var serialNumber: String
#ObservedObject var viewModel: BarcodeScannerViewModel
var body: some View {
let serialNumberBinding = Binding<String>(
get: { viewModel.barcodeString.isEmpty ? serialNumber : viewModel.barcodeString },
set: { serialNumber = viewModel.barcodeString.isEmpty ? $0 : viewModel.barcodeString }
)
TextField(NSLocalizedString("textfield.hint.device.id", comment: ""), text: serialNumberBinding)
.textFieldStyle(RoundedBorderTextFieldStyle())
.onChange(of: serialNumber, perform: { value in
serialNumber = value
})
}
private func textFieldChanged(_ text: String) {
print(text)
}
}
struct PairButton: View {
#ObservedObject var bluetoothConnectorViewModel: BluetoothConnectorViewModel
#ObservedObject var barcodeScannerViewModel: BarcodeScannerViewModel
#Binding var serialNumber: String
var body: some View {
Button(action: {
withAnimation {
bluetoothConnectorViewModel.connectBluetoothLowEnergyDevice(deviceID: serialNumber)
}
}) {
ButtonText(text: NSLocalizedString("button.text.pair", comment: ""))
}
}
}
So basically I want to make it so:
If user scans barcode, the serial is added to the textfield, press pair button to use that serial and call the method and pass in the serial number.
If user deletes the prepopulated serial, enters their own, it should update the textfield, and pressing the button should use the new serial number.
Right now when I scan a barcode, it populates the text field, however the onChange callback isn't picked up until I actually type in the TextField, so the result is never set for the method call.
Any help with this would be great, hope it makes sense.
You can create a binding with a custom closure, like this:
struct ContentView: View {
#State var location: String = ""
var body: some View {
let binding = Binding<String>(get: {
self.location
}, set: {
self.location = $0
// do whatever you want here
})
return VStack {
Text("Current location: \(location)")
TextField("Search Location", text: binding)
}
}
}

Passing an ObservableObject model through another ObObject?

I feel like I can sort of understand why what I'm doing isn't working but I'm still trying to wrap my head around Combine and SwiftUI so any help here would be welcome.
Consider this example:
Single view app that stores some strings in UserDefaults, and uses those strings to display some Text labels. There are three buttons, one to update the title, and one each to update the two UserDefaults-stored strings to a random string.
The view is a dumb renderer view and the title string is stored directly in an ObservableObject view model. The view model has a published property that holds a reference to a UserSettings class that implements property wrappers to store the user defined strings to UserDefaults.
Observations:
• Tapping "Set A New Title" correctly updates the view to show the new value
• Tapping either of the "Set User Value" buttons does change the value internally, however the view does not refresh.
If "Set A New Title" is tapped after one of these buttons, the new values are shown when the view body rebuilds for the title change.
View:
import SwiftUI
struct ContentView: View {
#ObservedObject var model = ViewModel()
var body: some View {
VStack {
Text(model.title).font(.largeTitle)
Form {
Section {
Text(model.settings.UserValue1)
Text(model.settings.UserValue2)
}
Section {
Button(action: {
self.model.title = "Updated Title"
}) { Text("Set A New Title") }
Button(action: {
self.model.settings.UserValue1 = "\(Int.random(in: 1...100))"
}) { Text("Set User Value 1 to Random Integer") }
Button(action: {
self.model.settings.UserValue2 = "\(Int.random(in: 1...100))"
}) { Text("Set User Value 2 to Random Integer") }
}
Section {
Button(action: {
self.model.settings.UserValue1 = "Initial Value One"
self.model.settings.UserValue2 = "Initial Value Two"
self.model.title = "Initial Title"
}) { Text("Reset All") }
}
}
}
}
}
ViewModel:
import Combine
class ViewModel: ObservableObject {
#Published var title = "Initial Title"
#Published var settings = UserSettings()
}
UserSettings model:
import Foundation
import Combine
#propertyWrapper struct DefaultsWritable<T> {
let key: String
let value: T
init(key: String, initialValue: T) {
self.key = key
self.value = initialValue
}
var wrappedValue: T {
get { return UserDefaults.standard.object(forKey: key) as? T ?? value }
set { return UserDefaults.standard.set(newValue, forKey: key) }
}
}
final class UserSettings: NSObject, ObservableObject {
let objectWillChange = PassthroughSubject<Void, Never>()
#DefaultsWritable(key: "UserValue", initialValue: "Initial Value One") var UserValue1: String {
willSet {
objectWillChange.send()
}
}
#DefaultsWritable(key: "UserBeacon2", initialValue: "Initial Value Two") var UserValue2: String {
willSet {
objectWillChange.send()
}
}
}
When I put a breakpoint on willSet { objectWillChange.send() } in UserSettings I see that the objectWillChange message is going to the publisher when I would expect it to so that tells me that the issue is likely that the view or the view model is not properly subscribing to it. I know that if I had UserSettings as an #ObservedObject on the view this would work, but I feel like this should be done in the view model with Combine.
What am I missing here? I'm sure it's really obvious...
ObsesrvedObject listens for changes in #Published property, but not the deeper internal publishers, so the below idea is to join internal publisher, which is PassthroughSubject, with #Published var settings, to indicate that the latter has updated.
Tested with Xcode 11.2 / iOS 13.2
The only needed changes is in ViewModel...
class ViewModel: ObservableObject {
#Published var title = "Initial Title"
#Published var settings = UserSettings()
private var cancellables = Set<AnyCancellable>()
init() {
self.settings.objectWillChange
.sink { _ in
self.objectWillChange.send()
}
.store(in: &cancellables)
}
}

How to observe a TextField value with SwiftUI and Combine?

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

Resources