onReceive in SwiftUI with array of ObservableObjects/State/Binding causes infinite loop - ios

I'm using .onReceive(_:perform:) to add pattern masks in my text field text. There is an infinite loop that happens when I use array of ObservableObjects, Binding, State objects or Published properties.
Example
struct MyTextField: View {
#Binding var text: String
...
}
extension MyTextField {
func mask(_ mask: String) -> some View {
self.onReceive(Just(text)) { newValue in
// `string(withMask:)` returns another string (already formatted correctly)
text = newValue.string(withMask: mask) // Obviously when I comment this line of code, the problem stops
}
}
}
Usage
final class FormItem: ObservableObject, Identifiable {
let id = UUID()
#Published var text = ""
let mask: String
init(mask: String) {
self.mask = mask
}
}
#State var volunteerForm: [FormItem] = [
FormItem(mask: "999.999.999-99")
]
var body: some View {
VStack {
ForEach(volunteerForm.indices, id: \.self) { index in
MyTextField("", text: volunteerForm[index].$text, onCommit: onCommit)
.mask(volunteerForm[index].mask)
}
}
}
But when I use a single property just like this #State var formItem: FormItem = ... this infinite loop doesn't happen. Also when I use an array of String instead of array of my Class FormItem, #State var volunteerTexts: [String] = [""], it doesn't happen too.
I wonder if this happen when we use a custom struct or class.
I've tried creating the model without ObservableObject and Published, just like a struct, but the infinite loop keeps happening:
struct FormItem: Identifiable {
let id = UUID()
var text = ""
let mask: String
}
VStack {
ForEach(volunteerForm.indices, id: \.self) { index in
TextField("", text: $volunteerForm[index].text, onCommit: onCommit)
.mask(volunteerForm[index].mask)
}
}
Do you have any ideia why is this infinite loop occurring?

Related

How to create textfields using Foreach

I need to create Textfield every time i click on button, but when i use foreach to create that, when i write smth. in new textfield it is written also in others. I want that every new textfield will be different that i could write different thing in each of them.
This is what i am using.
#State var arr: [String] = []
#StateObject var homePageVM: HomePageViewModel = HomePageViewModel()
ForEach(arr, id: \.self) { item in
TextField("text", text: item)
}
and in button click
Button {
arr.append($homepageVM.textfieldText)
} label: {
Text("Button")
}
How can i solve this?
struct TextItem: Identifiable {
let id = UUID()
var text: String = ""
}
class Model: ObservableObject {
#Published var textItems: [TextItem] = []
func addTextItem() {
textItems.append(TextItem())
}
// funcs for loading and save model data
}
#StateObject var model = Model()
ForEach($model.textItems) { $item in
TextField("text", text: $item.text)
}
Button("Add") {
model.addTextItem()
}

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

Cannot remove element from foreach if list row contains a textField in SwiftUI

I have read several solutions to this issue which I believe I am doing correctly based on suggestions but this simple code still crashes.
import SwiftUI
struct AnItem : Identifiable {
let id = UUID()
var text: String
}
struct ContentView: View {
#State private var items : [AnItem] = [AnItem(text: "A"), AnItem(text: "B"), AnItem(text: "C")]
var body: some View {
List () {
ForEach (0 ..< items.count) {
index in
TextField("test", text: $items[index].text)
}.onDelete(perform: deleteItem)
}
}
private func deleteItem(at indexSet: IndexSet) {
self.items.remove(atOffsets: indexSet)
}
}
Anybody have an idea for a workaround, or maybe I'm just doing this wrong. The crash says
Fatal error: Index out of range: file Swift/ContiguousArrayBuffer.swift, line 444
As others have stated, just a Text widget works fine. This appears to be induced by using TextField.
Here is a possible solution. Using id:\.id to identity the items and then extract the TextField to another view
struct AnItem : Identifiable {
let id = UUID()
var text: String
}
struct ContentView: View {
#State private var items : [AnItem] = [AnItem(text: "A"), AnItem(text: "B"), AnItem(text: "C")]
var body: some View {
List {
ForEach(items, id: \.id) { item in
CustomTextField(item: item)
}.onDelete(perform: deleteItem)
}
}
private func deleteItem(at indexSet: IndexSet) {
items.remove(atOffsets: indexSet)
}
}
struct CustomTextField : View {
#State var item : AnItem
var body : some View {
TextField("Test", text: $item.text)
}
}
How about doing it like this:
First lets make AnItem conform to Hashable protocol.
struct AnItem : Identifiable, Hashable {
let id = UUID()
var text: String
}
Now let's change your Foreach and List a little bit:
List {
ForEach (items, id: \.self) { item in
Text("\(item.text)")
}.onDelete(perform: deleteItem)
}
Voila! everything is working perfectly now!

SwiftUI - Getting TextField to array in ForEach

The goal is to have a TextField that takes characters as they're being entered and display them below the TextField in a 'pile'. By pile I mean, each new character is displayed on top of the last one. Piling the letters up is easy enough - I use ForEach to loop over an array inside a ZStack. To get the characters in the array, I used a custom binding. It works, but the problem is that the characters get repeated each time a new character is typed. For example, if the user types CAT, first the C will appear, then CA on top of the C, then CAT on top of the CA and the C. In other words, there are six characters stacked up when I only wanted three.
struct ContentView: View {
#State private var letter = ""
#State private var letterArray = [String]()
var body: some View {
let binding = Binding<String>(
get: { self.letter },
set: { self.letter = $0
self.letterArray.append(self.letter)
})
return VStack {
TextField("Type letters and numbers", text: binding)
ZStack {
ForEach(letterArray, id: \.self) { letter in
Text(letter)
}
}
}
}
}
UPDATE:
I was making the problem a little more difficult that it was. By using an ObservableObject I was able to separate the logic from the view AND simplify the code. First I created a view model. Now, each time the user types something into a TextField, it's it's caught by didSet and converted into an array of characters. Note, I had to use map to convert from a character array to a string array because ForEach doesn't work with characters.
class ViewModel: ObservableObject {
#Published var letterArray = [String]()
#Published var letter = "" {
didSet {
letterArray = Array(letter).map { String($0) }
}
}
}
In ContentView, I only need #ObservedObject var vm = ViewModel()Then I refer to the variables using vm.letter or vm.letterArray
Uptown where I get your problem may the below code help you
I modified your code as below;
struct ContentView: View {
#State private var letter = ""
#State private var letterCounter = 0
#State private var letterArray = [String]()
var body: some View {
let binding = Binding<String>(
get: { self.letter },
set: { self.letter = $0
if self.letter.count > self.letterCounter {
if let lastLetter = self.letter.last{
self.letterArray.append(String(lastLetter))
}
}else{
_ = self.letterArray.removeLast()
}
self.letterCounter = self.letter.count
})
return VStack {
TextField("Type letters and numbers", text: binding)
VStack {
ForEach(letterArray, id: \.self) { letter in
Text(letter)
}
}
}
}
}

SwiftUI Picker in Form does not show checkmark

I have a Picker embedded in Form, however I can't get it working that it shows a checkmark and the selected value in the form.
NavigationView {
Form {
Section {
Picker(selection: $currencyCode, label: Text("Currency")) {
ForEach(0 ..< codes.count) {
Text(self.codes[$0]).tag($0)
}
}
}
}
}
TL;DR
Your variable currencyCode does not match the type of the ID for each element in your ForEach. Either iterate over the codes in your ForEach, or pass your Picker an index.
Below are three equivalent examples. Notice that the #State variable which is passed to Picker always matches the ID of element that the ForEach iterates over:
Also note that I have picked a default value for the #State variable which is not in the array ("", -1, UUID()), so nothing is shown when the form loads. If you want a default option, just make that the default value for your #State variable.
Example 1: Iterate over codes (i.e. String)
struct ContentView: View {
#State private var currencyCode: String = ""
var codes: [String] = ["EUR", "GBP", "USD"]
var body: some View {
NavigationView {
Form {
Section {
Picker(selection: $currencyCode, label: Text("Currency")) {
// ID is a String ----v
ForEach(codes, id: \.self) { (string: String) in
Text(string)
}
}
}
}
}
}
}
Example 2: Iterate over indices (i.e. Int)
struct ContentView: View {
#State private var selectedIndex: Int = -1
var codes: [String] = ["EUR", "GBP", "USD"]
var body: some View {
NavigationView {
Form {
Section {
Picker(selection: $selectedIndex, label: Text("Currency")) {
// ID is an Int --------------v
ForEach(codes.indices, id: \.self) { (index: Int) in
Text(self.codes[index])
}
}
}
}
}
}
}
Example 3: Iterate over an identifiable struct by its ID type (i.e. UUID)
struct Code: Identifiable {
var id = UUID()
var value: String
init(_ value: String) {
self.value = value
}
}
struct ContentView: View {
#State private var selectedUUID = UUID()
var codes = [Code("EUR"), Code("GBP"), Code("USD")]
var body: some View {
NavigationView {
Form {
Section {
Picker(selection: $selectedUUID, label: Text("Currency")) {
// ID is a UUID, because Code conforms to Identifiable
ForEach(self.codes) { (code: Code) in
Text(code.value)
}
}
}
}
}
}
}
It's difficult to say, what you are doing wrong, because your example doesn't include the declaration of codes or currencyCode. I suspect that the problem is with the binding being of a different type than the tag you are setting on a picker (which is an Int in your case).
Anyway, this works:
struct ContentView: View {
let codes = Array(CurrencyCode.allCases)
#State private var currencyCode: CurrencyCode?
var body: some View {
NavigationView {
Form {
Section {
Picker("Currency",
selection: $currencyCode) {
ForEach(codes, id: \.rawValue) {
Text($0.rawValue).tag(Optional<CurrencyCode>.some($0))
}
}
}
}
}
}
}
enum CurrencyCode: String, CaseIterable {
case eur = "EUR"
case gbp = "GBP"
case usd = "USD"
}

Resources