SwiftUI TextField onChange not triggered - ios

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

Related

SwiftUI extract Childview with Bindings, #State

I have the following component in a view
HStack {
TextField("New Note", text: $newNoteContent)
.onSubmit(submitNewNote)
.focused($newNoteIsFocused)
if (!newNoteContent.isEmpty) {
Button(action: submitNewNote) {
Image(systemName: "checkmark")
}
}
}
The variables are defined as follows
#State private var newNoteContent: String = ""
#FocusState private var newNoteIsFocused: Bool
func submitNewNote() {
Note.add(content: newNoteContent)
newNoteContent = ""
newNoteIsFocused = false
}
I would like to extract it and make it either a computed variable returning a view or a function that returns a view (I dont know which is better). I want to extract it because I reuse a similar struct.
Full code in case its needed: https://github.com/charelF/RemindMeApp/blob/main/RemindMe/NotesView.swift
I have tried the following:
func editCell(
noteContent: Binding<String>,
submitFunc: #escaping () -> (),
focus: FocusState<Bool>.Binding
) -> some View {
return HStack {
TextField("New Note", text: noteContent)
.onSubmit(submitFunc)
.focused(focus)
if (!noteContent.isEmpty) {
Button(action: submitFunc) {
Image(systemName: "checkmark")
}
}
}
}
But there are some errors and its generally just playing around -- I have no idea what I am really doing here and need some feedback/help.
Update from comment:
So I extracted the view as follows for now
struct ExtractedView: View {
#State private var editNoteContent: String = ""
#FocusState private var editNoteIsFocused: Bool
#State private var editNote: Note? = nil
func editExistingNote(note: Note?) {
guard let note else { return }
note.content = editNoteContent
PersistenceController.shared.save()
editNoteContent = ""
editNoteIsFocused = false
editNote = nil
}
But I dont understand how to call it. If I call it with ExtractedView() then the code compiles and the app runs, but it crashes when I enter this path of the app. And when I call it like this:
ExtractedView(
editNoteContent: editNoteContent,
editNoteIsFocused: editNoteIsFocused,
editNote: editNote
)
Then i get lots of errors ...
Thanks to the comments and this answer here: https://developer.apple.com/forums/thread/682448 I got it to work. I extracted the view as follows:
struct EditNoteView: View {
#Binding var noteContent: String
#FocusState var isNoteFocused: Bool
var onsubmit: () -> ()
var placeholder: String
var body: some View {
HStack {
TextField(placeholder, text: $noteContent)
.onSubmit(onsubmit)
.focused($isNoteFocused)
if (!noteContent.isEmpty) {
Button(action: onsubmit) {
Image(systemName: "checkmark")
}
}
}
}
}
and then I call this child view from my parent view (inside the body) with
EditNoteView(
noteContent: $newNoteContent,
isNoteFocused: _newNoteIsFocused,
onsubmit: submitNewNote,
placeholder: "New Note"
)
Also in my parent views, I have the following definitions for the variables
#State private var newNoteContent: String = ""
#FocusState private var newNoteIsFocused: Bool
func submitNewNote() {
Note.add(content: newNoteContent)
newNoteContent = ""
newNoteIsFocused = false
}
The main takeaways is that all of the #State things map to #Binding in the childview, and the #FocusState maps to another #FocusState, but there is a _ required before the parameter in the call.

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

Importing the data typed by the user to a view?

In the process of making my first Finance App, I want the user to type their Credit Card Name and las four numbers (probably more info since this is a draft) into this Modally presented view, to then be seen in a cards index, widget-look-like.
struct CardListView: View {
#State var isPresentingAddModal = false
#State var emisorTarjeta = ""
#State var numeroTarjeta = ""
var headerView: some View {
HStack {
Text("Tus tarjetas")
Spacer()
Button("Añadir nueva") {
self.isPresentingAddModal.toggle()
}
.sheet(isPresented: $isPresentingAddModal, content: {
HStack {
Text("Emisor de tarjeta")
TextField("Seleccionar emisor de tarjeta", text: $emisorTarjeta)
}
HStack {
Text("Número de tarjeta")
TextField("Escribí tu número de tarjeta", text: $numeroTarjeta)
}
Button(action: {
self.isPresentingAddModal.toggle()
print("\(self.emisorTarjeta)")
}, label: {
Text("Añadir")
})
Spacer()
})
}
The question now is how to pass the info typed from the two textFields, to the view where the cards will be created. The button "Añadir" currently works as a dismiss button instead of an add one, since I don't know how to create that.
(Also, a lot of code like paddings and backgroundColors have been erased to make it clearer to see)
Enitre view of the homeView
Where the "añadir" button is
there are several ways to do this. One simple way is to use "#State" and "#Binding" like this:
In "CardListView" use this:
#Binding var emisorTarjeta: String
#Binding var numeroTarjeta: String
and in the "CardViewCreator" use:
#State var emisorTarjeta = ""
#State var numeroTarjeta = ""
Another way is to use "ObservableObject", create a class like this:
class CardModel: ObservableObject {
#Published var emisorTarjeta = ""
#Published var numeroTarjeta = ""
}
In the your "CardViewCreator" or some parent view:
#StateObject var cardModel = CardModel()
and pass it to the "CardListView" like this:
struct CardListView: View {
#ObservedObject var cardModel: CardModel
...
}
You can also use "EnvironmentObject" in a similar way.
It all depends on your case. I recommend reading up on "ObservedObject"
and using that.
A really simple way of doing this is to pass in a closure to run when the add button is tapped. Here's an example, which also shows how to dismiss the presented sheet
import SwiftUI
struct Card: Identifiable {
let id = UUID()
let provider: String
let number: String
}
struct ContentView: View {
#State private var cards = [Card]()
#State private var showingSheet = false
var body: some View {
VStack {
List(cards, rowContent: CardView.init)
.padding(.bottom, 10)
Button("Add") {
showingSheet = true
}
.padding()
}
.sheet(isPresented: $showingSheet) {
AddSheet(completion: addCard)
}
}
func addCard(provider: String, number: String) {
let newCard = Card(provider: provider, number: number)
cards.append(newCard)
}
}
struct CardView: View {
let card: Card
var body: some View {
VStack(alignment: .leading) {
Text(card.provider)
Text(card.number)
}
}
}
struct AddSheet: View {
#Environment(\.presentationMode) var presentationMode
#State private var provider = ""
#State private var number = ""
let completion: (String, String) -> Void
var body: some View {
VStack {
TextField("Provider", text: $provider).padding()
TextField("Number", text: $number).padding()
Button("Add") {
completion(provider, number)
presentationMode.wrappedValue.dismiss()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
If you want to actually save the information passed in the textfield you would have to save it somewhere and later fetch it when required But this is only if you want to be able to access the information passed into the cards index after you have closed down the application and opened it up once again.

What textfield / view to show and edit text in SwiftUI?

I would like to edit an item, for example, I have an item named "Jacket", then I would like to edit with a new name "Gray Jacket"
Now I'll have the list of my items, and when I press the "Jacket" item, it will go to the DetailItem and when it appears, I want that textfield / view already filled with "Jacket" text then I able to edit it. Please note I want to use real text not placeholder when the item name first shows up.
Does anyone know what element should I use, or if textfield how? Because I couldn't find how to set textfield with name when it first appears.
UPDATE:
Here's what I've tried, this code I took it from here. It basically took the previous text and make it a placeholder, while actually I want it to be editable.
struct CustomTextField: View {
var placeholder: Text
#Binding var text: String
var editingChanged: (Bool)->() = { _ in }
var commit: ()->() = { }
var body: some View {
ZStack(alignment: .leading) {
if text.isEmpty { placeholder }
TextField("", text: $text, onEditingChanged: editingChanged, onCommit: commit)
}
}
}
struct UsageCustomTxtField: View {
#State var text = ""
var body: some View {
CustomTextField(
placeholder: Text("placeholder").foregroundColor(.black),
text: $text
)
}
}
Here is a possible solution:
import SwiftUI
struct ContentView: View {
#ObservedObject var myItems: ItemViewModel = ItemViewModel()
#State var myNewName: String = "" // Will set the new name
var body: some View {
VStack {
TextField(self.myItems.myList[0].name, // when it appears, I want that textfield / view already filled with "Jacket" text
text: self.$myNewName,
onCommit: {
self.myItems.changeNameOfListItemTo(newName: self.myNewName, index: 0) I would like to edit with a new name "Gray Jacket"
self.myNewName = "" //empty the textfield again so name from item itself will be displayed
})
}
}
}
struct Item { // I have an item named "Jacket"
var name: String
init(name: String) {
self.name = name
}
}
class ItemViewModel: ObservableObject {
#Published var myList: Array<Item> = [Item(name: "Jacket")] //your item list
func changeNameOfListItemTo(newName: String, index: Int) { //function to change the name of an item
self.myList[0].name = newName
}
}

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

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

Resources