SwiftUI - Getting TextField to array in ForEach - 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)
}
}
}
}
}

Related

Swift dynamically create editable modal from Stepper component?

I have a Stepper component that corresponds to number of people (for now, I don't want to restrict what the stepper goes up to, but users will likely never enter more than 10). I want to dynamically generate a box that is essentially a Person object/class based on the current value of Stepper.
Current code
struct ContentView: View {
#State private var numPeople = 0
var body: some View {
VStack {
Text("Enter number of people below:")
Stepper("\(numPeople.formatted()) People", value: $numPeople)
}
}
Desired outcome if user clicks plus button until the number 3.
Should this be done using a for loop (or ForEach loop)? Should a component other than Stepper be used?
There are a few different ways of doing this but for growth sake you can consider a PersonModel and basing the stepper on the count of Array<PersonModel> using a computed variable that adds and removes from the array.
import SwiftUI
struct PersonModel: Identifiable{
let id: UUID = .init()
let name: String
}
class PersonVM: ObservableObject{
#Published var people: [PersonModel] = []
var personCount: Int{
get{
//Single source of truth.
people.count
}
set{
let diff: Int = people.count - newValue
if diff == 0{
//No change
} else if diff < 0 { //Add the difference
let new: [PersonModel] = (0..<abs(diff)).map{ n in
PersonModel(name: "Person \(people.count + n + 1)")
}
people.append(contentsOf: new)
} else if !people.isEmpty{ //Remove the difference if the array isn't empty
people.removeLast(diff)
}
}
}
}
#available(iOS 15.0, *)
struct PersonView: View {
#StateObject private var vm: PersonVM = .init()
var body: some View {
ScrollView {
Text("Enter number of people below:")
Stepper("\(vm.people.count.formatted()) People", value: $vm.personCount, in: 0...10, step: 1)
ForEach(vm.people) { person in
ZStack{
Color.blue
Text(person.name)
.foregroundColor(.white)
}.padding(2)
}
}
}
}

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

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?

Read and make calculations with textfield input SwiftUI

I want to make calculations with inputs from 2 different Textfields and put the output in a Text. See code:
#State var input1: String = ""
#State var input2: String = ""
var calculation : Double {
let calculationProduct = Double(input1) * Double(input2)
return calculationProduct
}
var body: some View {
VStack{
TextField("", text: $input1)
TextField("", text: $input1)
Text("\(calculation)")
}
The problem is the code won't compile, i get different compile errors, for example: "Binary operator '*' cannot be applied to two 'Double?' operands".
What goes wrong?
Double(input1) returns String? because it's not guaranteed to work. e.g. Double("1abc")
We can use guard let or if let or even a nil coalescing operator ?? to handle this. But for the following example we will gracefully handle it using guard let.
struct ContentView: View {
#State var input1: String = ""
#State var input2: String = ""
var calculation : Double {
guard let m = Double(input1), let n = Double(input2) else { return 0 }
return m * n
}
var body: some View {
VStack {
TextField("", text: $input1)
TextField("", text: $input2)
Text("\(calculation)")
}
}
}
EDIT
As per your comments, there are multiple ways to show "Error" on invalid inputs, or the answer upto 2 decimal point.
For this example, lets change the result to a computed String property both these these cases, as such:
struct ContentView: View {
#State var input1: String = ""
#State var input2: String = ""
var calculation: String {
//check if both fields have text else no need for message
guard input1.isEmpty == false, input2.isEmpty == false else { return "" }
//check if both are numbers else we need to print "Error"
guard let m = Double(input1), let n = Double(input2) else { return "Error" }
let product = m * n
return String(format: "%.2f", product)
}
var body: some View {
VStack {
TextField("Enter First Number", text: $input1)
.textFieldStyle(RoundedBorderTextFieldStyle())
TextField("Enter Second Number", text: $input2)
.textFieldStyle(RoundedBorderTextFieldStyle())
Text(calculation)
}
}
}
PS: If you want to ensure only numbers can be typed then you should think about applying the .keyboardType(.decimalPad) modifier on the TextFields.
#Johankornet, you have probably figured this out by now, but the below code will work for decimal entries:
import SwiftUI
struct ContentView: View {
#State var textFieldEntry: Double = 1.0
let formatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
return formatter
}()
var body: some View {
VStack {
TextField("title", value: $textFieldEntry, formatter: formatter).padding()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
To see it working, i made an entire app with this main file:
import SwiftUI
#main
struct testTextFieldWithNumericFormattingApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
formatter.decimalstyle = .decimal restricts the TextField input to decimals only. Type in something thats not a decimal, like "abc", hit return and the input is ignored. This input field can accept scientific notation numbers as input, but the TextField will display as a decimal so very small scientific notation inputs, such as 3e-7 will be displayed as zero.
#JohanKornet, you have probably found other more elegant solutions for this problem by now, but if not maybe this will help:)

SwiftUI View not updating based on #ObservedObject

In the following code, an observed object is updated but the View that observes it is not. Any idea why?
The code presents on the screen 10 numbers (0..<10) and a button. Whenever the button is pressed, it randomly picks one of the 10 numbers and flips its visibility (visible→hidden or vice versa).
The print statement shows that the button is updating the numbers, but the View does not update accordingly. I know that updating a value in an array does not change the array value itself, so I use a manual objectWillChange.send() call. I would have thought that should trigger the update, but the screen never changes.
Any idea? I'd be interested in a solution using NumberLine as a class, or as a struct, or using no NumberLine type at all and instead rather just using an array variable within the ContentView struct.
Here's the code:
import SwiftUI
struct ContentView: View {
#ObservedObject var numberLine = NumberLine()
var body: some View {
VStack {
HStack {
ForEach(0 ..< numberLine.visible.count) { number in
if self.numberLine.visible[number] {
Text(String(number)).font(.title).padding(5)
}
}
}.padding()
Button(action: {
let index = Int.random(in: 0 ..< self.numberLine.visible.count)
self.numberLine.objectWillChange.send()
self.numberLine.visible[index].toggle()
print("\(index) now \(self.numberLine.visible[index] ? "shown" : "hidden")")
}) {
Text("Change")
}.padding()
}
}
}
class NumberLine: ObservableObject {
var visible: [Bool] = Array(repeatElement(true, count: 10))
}
With #ObservedObject everything's fine... let's analyse...
Iteration 1:
Take your code without changes and add just the following line (shows as text current state of visible array)
VStack { // << right below this
Text("\(numberLine.visible.reduce(into: "") { $0 += $1 ? "Y" : "N"} )")
and run, and you see that Text is updated so observable object works
Iteration 2:
Remove self.numberLine.objectWillChange.send() and use instead default #Published pattern in view model
class NumberLinex: ObservableObject {
#Published var visible: [Bool] = Array(repeatElement(true, count: 10))
}
run and you see that update works the same as on 1st demo above.
*But... main numbers in ForEach still not updated... yes, because problem in ForEach - you used constructor with Range that generates constant view's group by-design (that documented!).
!! That is the reason - you need dynamic ForEach, but for that model needs to be changed.
Iteration 3 - Final:
Dynamic ForEach constructor requires that iterating data elements be identifiable, so we need struct as model and updated view model.
Here is final solution & demo (tested with Xcode 11.4 / iOS 13.4)
struct ContentView: View {
#ObservedObject var numberLine = NumberLine()
var body: some View {
VStack {
HStack {
ForEach(numberLine.visible, id: \.id) { number in
Group {
if number.visible {
Text(String(number.id)).font(.title).padding(5)
}
}
}
}.padding()
Button("Change") {
let index = Int.random(in: 0 ..< self.numberLine.visible.count)
self.numberLine.visible[index].visible.toggle()
}.padding()
}
}
}
class NumberLine: ObservableObject {
#Published var visible: [NumberItem] = (0..<10).map { NumberItem(id: $0) }
}
struct NumberItem {
let id: Int
var visible = true
}
I faced the same issue.
For me, replacing #ObservedObject with #StateObject worked.
Using your insight, #Asperi, that the problem is with the ForEach and not with the #ObservableObject functionality, here's a small modification to the original that does the trick:
import SwiftUI
struct ContentView: View {
#ObservedObject var numberLine = NumberLine()
var body: some View {
VStack {
HStack {
ForEach(Array(0..<10).filter {numberLine.visible[$0]}, id: \.self) { number in
Text(String(number)).font(.title).padding(5)
}
}.padding()
Button(action: {
let index = Int.random(in: 0 ..< self.numberLine.visible.count)
self.numberLine.visible[index].toggle()
}) {
Text("Change")
}.padding()
}
}
}
class NumberLine: ObservableObject {
#Published var visible: [Bool] = Array(repeatElement(true, count: 10))
}
There is nothing Wrong with observed object, you should use #Published in use of observed object, but my code works without it as well. And also I updated your logic in your code.
import SwiftUI
struct ContentView: View {
#ObservedObject var model = NumberLineModel()
#State private var lastIndex: Int?
var body: some View {
VStack(spacing: 30.0) {
HStack {
ForEach(0..<model.array.count) { number in
if model.array[number] {
Text(String(number)).padding(5)
}
}
}
.font(.title).statusBar(hidden: true)
Group {
if let unwrappedValue: Int = lastIndex { Text("Now the number " + unwrappedValue.description + " is hidden!") }
else { Text("All numbers are visible!") }
}
.foregroundColor(Color.red)
.font(Font.headline)
Button(action: {
if let unwrappedIndex: Int = lastIndex { model.array[unwrappedIndex] = true }
let newIndex: Int = Int.random(in: 0...9)
model.array[newIndex] = false
lastIndex = newIndex
}) { Text("shuffle") }
}
}
}
class NumberLineModel: ObservableObject {
var array: [Bool] = Array(repeatElement(true, count: 10))
}
The problem is with the function, do not forget to add id: \.self in your ForEach function, and make your Model Hashable, Identifiable.

How to initialize derived variables in body methods in SwiftUI (or alternate approach)

I'm trying to figure out the right way to initialized derived variables in the body method for a SwiftUI view. An example would the string value for an editable integer which would then be edited in a TextField. The integer could for example be part of an #ObservedObject. I cannot figure out any remotely clean way to do this.
I've looked into using custom initializers but this does not seem like the right thing to do. I'm not even sure this code would be run at the appropriate time.
I've also tried using the .onAppear method for TextField, but this method does not appear to be re-executed when the view is rebuilt.
simplified example:
final class Values : ObservableObject {
#Published var count: Int = 0;
}
var sharedValues = Values()
struct ContentView : View {
#ObservedObject var values = sharedValues
var body: some View {
VStack {
Button(
action: { self.add() },
label: { Text("Plus")}
)
InnerView()
}
}
func add() { values.count += 1 }
}
struct InnerView : View {
#ObservedObject var values = sharedValues
#State private var text = ""
var body: some View {
// text = String(value.count) - what I want to do
TextField("", text: $text, onEditingChanged: updateCount)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
func updateCount(updated: Bool) { /* this isn't important in this context */}
}
I would hope to be able to update sharedValues externally and see the update in MyView. In this example, I would want pressing the button to update the text field with the updated text value. But I can't figure a way to have the string representation of the count value computed at the appropriate point in the execution of the code.
I've tried multiple approaches to achieving this type of result, but have come up short.
I'm not sure if I'm understanding your question correctly, but if you are just trying to be able to change a number with a button, have the number be displayed in a text field, and then be able to edit it there, you don't need an ObserverableObject or multiple views.
Here is an example of how you can do it:
struct ContentView: View {
#State var count = 0
#State var countStr = ""
var body: some View {
VStack {
Button(action: {
self.count += 1
self.countStr = "\(self.count)"
}) {
Text("Plus")
}
TextField("", text: $countStr, onEditingChanged: updateCount)
}
}
func updateCount(updated: Bool) { /* this isn't important in this context */ }
}
Use value init method of TextField. This take the value as 2 way Binding. So it automatically update count from both text field and buttons.
import SwiftUI
import Combine
final class Values : ObservableObject {
#Published var count: Int = 0;
}
var sharedValues = Values()
struct AndrewVoelkel : View {
#ObservedObject var values = sharedValues
var body: some View {
HStack {
InnerView()
VStack{
Button(
action: { self.add() },
label: { Text("+")}
)
Button(
action: { self.sub() },
label: { Text("-")}
)
}.font(.headline)
}.padding()
}
func add() { values.count += 1 }
func sub() { values.count -= 1 }
}
struct InnerView : View {
#ObservedObject var values = sharedValues
var body: some View {
TextField("", value: $values.count, formatter: NumberFormatter())
.textFieldStyle(RoundedBorderTextFieldStyle())
}
}

Resources