SwiftUI onReceive causing infinite loop on an embedded structure - swiftui

I need to pass a list of choices and their choice IDs to a form to edit the list of choices. I'm able to edit these choices with no issues.
When I add an onReceive function to limit the number of characters to 30, the onReceive goes into an infinite loop. If I used an array of Strings, I do not get the infinite loop.
I want to keep the data in a struct to pass around my program.
I have simplified the code below to duplicate the issue, any advice would be greatly appreciated:
struct ContentView: View {
struct ChoiceDataObject {
var choiceId: Int
var choice: String
}
public struct dataParameter {
var choices:[ChoiceDataObject] = [
ChoiceDataObject(choiceId: 100, choice: "Choice 1"),
ChoiceDataObject(choiceId: 200, choice: "Choice 2")
]
var simpleArray: [String] = ["Choice 1", "Choice 2"]
}
#State var data = dataParameter()
var body: some View {
TextField("Enter some text", text: $data.choices[0].choice)
.onReceive(data.choices[0].choice.publisher.collect()) {
data.choices[0].choice = String($0.prefix(30)) // <-causing infinate loop
}
TextField("Enter some text", text: $data.simpleArray[0])
.onReceive(data.simpleArray[0].publisher.collect()) {
data.simpleArray[0] = String($0.prefix(30)) // <- this works
}
}
}

Related

Iterating an object array and changing values?

Used this scenario as an example to be a bit more self explanatory. I have a struct which represents a character, and one of the structs attributes is another struct: Stats (where I used id to represent the name in a more simple way).
Besides that I have a view with a ForEach, where I iterate over some Characters and I need to be able to increase a specific stat.
The problem is: I'm trying to increase stat.points using a button, but I keep getting the message "Left side of mutating operator isn't mutable: 'product' is a 'let' constant".
struct Character: Identifiable {
var id: String
var name: String
var stats: [Stats]
}
struct Stats: Identifiable {
var id: String
var points: Int
}
ForEach(characters.stats) { stat in
HStack {
Text("\(stat.id)")
Button {
stat.points += 1
} label: {
Text("Increase")
}
}
}
How could I make this work?
As you have found, structs in your stats: [Stats] does not allow changes
especially in a ForEach where it is a let.
There are many ways to achieve what you want, depending on what you trying to do.
This example code shows the most basic way, using the array of stats: [Stats]
directly:
struct ContentView: View {
#State var characters = Character(id: "1",name: "name-1", stats: [Stats(id: "1", points: 1),Stats(id: "2", points: 1)])
var body: some View {
ForEach(characters.stats.indices, id: \.self) { index in
HStack {
Text("\(characters.stats[index].id)")
Button {
characters.stats[index].points += 1
} label: {
Text("Increase")
}
Text("\(characters.stats[index].points)")
}
}
}
}
Here is another approach, using a function, to increase your stat points value:
struct ContentView: View {
#State var characters = Character(id: "1", name: "name-1", stats: [Stats(id: "1", points: 1), Stats(id: "2", points: 1)])
var body: some View {
ForEach(characters.stats) { stat in
HStack {
Text("\(stat.id)")
Button {
increase(stat: stat)
} label: {
Text("Increase")
}
Text("\(stat.points)")
}
}
}
func increase(stat: Stats) {
if let index = characters.stats.firstIndex(where: {$0.id == stat.id}) {
characters.stats[index].points += 1
}
}
}

SwiftIU, I want to reset my list (remove all list items) when 10 items added to the list

struct ContentView: View {
var array = ["Fire", "Water", "Earth", "Air", "Emotion"]
#State var randomList = [String]()
var body: some View {
VStack {
List (randomList, id: \.self) { ranList in
Text(ranList)
}
Button("Add to list") {
let ranIndexNumber = Int.random(in: 0...array.count-1)
randomList.append(array[ranIndexNumber])
}
Hi everyone, I am new to the coding and to stack overflow website, I am learning SwiftUI atm and this code above was a "challenge" which is; as I tap the button, the code adds random items from array list above, to the list. I managed to write the code, but I wanted to add another function to the button;
When tapped, in addition to adding items to the list, I want it to remove all items if item count in the list exceed 10. How am I suppose to do that, I don't know/not yet learned a way to count the items in a list to use in an IF statement.
Thanks for the help in advance
P.S. if there is any way to add my code with right colors while posting, I would be grateful if you shortly describe how to do it.Thanks
struct ContentView: View {
var array = ["Fire", "Water", "Earth", "Air", "Emotion"]
#State var randomList = [String]()
var body: some View {
VStack {
List (randomList, id: \.self) { ranList in
Text(ranList)
}
Button("Add to list") {
let ranIndexNumber = Int.random(in: 0...array.count-1)
if randomList.count >= 10 {
randomList = [array[ranIndexNumber]]
} else {
randomList.append(array[ranIndexNumber])
}
}

Why does this SwiftUI code show "Empty" in the destination screen?

struct ContentView: View {
#State var showModal = false
#State var text = "Empty"
var body: some View {
Button("show text") {
text = "Filled"
showModal = true
}
.sheet(isPresented: $showModal) {
VStack {
Text(text)
Button("print text") {
print(text)
}
}
}
}
}
I thought that when the "show text" button was tapped, the value of text would be set to "Filled" and showModal would be set to true, so that the screen specified in sheet would be displayed and the word "Filled" would be shown on that screen.
I thought it would show "Filled", but it actually showed "Empty".
Furthermore, when I printed the text using the print text button, the console displayed "Filled".
Why does it work like this?
What am I missing to display the value I set when I tap the button on the destination screen?
using Xcode12.4, Xcode12.5
Add the code for the new pattern.
struct ContentView: View {
#State var number = 0
#State var showModal = false
var body: some View {
VStack {
Button("set number 1") {
number = 1
showModal = true
print("set number = \(number)")
}
Button("set number 2") {
number = 2
showModal = true
print("set number = \(number)")
}
Button("add number") {
number += 1
showModal = true
print("add number = \(number)")
}
}
.sheet(isPresented: $showModal) {
VStack {
let _ = print("number = \(number)")
Text("\(number)")
}
}
}
}
In the above code, when I first tap "set number 1" or "set number 2", the destination screen shows "0". No matter how many times you tap the same button, "0" will be displayed.
However, if you tap "set number 2" after tapping "set number 1", it will work correctly and display "2". If you continue to tap "set number 1", "1" will be displayed and the app will work correctly.
When you tap "add number" for the first time after the app is launched, "0" will still be displayed, but if you tap "add number" again, "2" will be displayed and the app will count up correctly.
This shows that the rendering of the destination screen can be started even when the #State variable is updated, but only when the #State variable is referenced first in the destination screen, it does not seem to be referenced properly.
Can anyone explain why it behaves this way?
Or does this look like a bug in SwiftUI?
Since iOS 14, there are a couple of main ways of presenting a Sheet.
Firstly, for your example, you need to create a separate View and pass your property to a Binding, which will then be correctly updated when the Sheet is presented.
// ContentView
Button { ... }
.sheet(isPresented: $showModal) {
SheetView(text: $text)
}
struct SheetView: View {
#Binding var text: String
var body: some View {
VStack {
Text(text)
Button("print text") {
print(text)
}
}
}
}
The other way of doing it is by using an Optional identifiable object, and when that object has a value the sheet will be presented. Doing that, you do not need to separately manage the state of whether the sheet is showing.
struct Item: Identifiable {
var id = UUID()
var text: String
}
struct ContentView: View {
#State var item: Item? = nil
var body: some View {
Button("show text") {
item = Item(text: "Filled")
}
.sheet(item: $item, content: { item in
VStack {
Text(item.text)
Button("print text") {
print(item.text)
}
}
})
}
}
By adding the following code, I was able to display the word "Filled" in the destination screen.
Declare the updateDetector as a #State variable.
Update the updateDetector onAppear of the View in the sheet.
Reference the updateDetector somewhere in the View in the sheet.
struct ContentView: View {
#State var showModal = false
#State var updateDetector = false
#State var text = "Empty"
var body: some View {
Button("show text") {
text = "Filled"
print("set text to \(text)")
showModal = true
}
.sheet(isPresented: $showModal) {
let _ = print("render text = \(text)")
VStack {
Text(text)
// use EmptyView to access updateDetector
if updateDetector {
EmptyView()
}
}
.onAppear {
// update updateDetector
print("toggle updateDetector")
updateDetector.toggle()
}
}
}
}
Then, after rendering the initial value at the time of preloading, the process of updating the updateDetector onAppear will work, the View will be rendered again, and the updated value of the #State variable will be displayed.
When you tap the "show text" button in the above code, the following will be displayed in the console and "Filled" will be shown on the screen.
set text to Filled
render text = Empty
toggle updateDetector
render text = Filled
I'm still not convinced why I can't get the updated value of the #State variable in the first preloaded sheet. So I will report this to Apple via the Feedback Assistant.

SwiftUI value in text doesn’t change

I don’t understand why the Text Value doesn’t change. if I remove the TextField, the Text value change :/ is there something about combine or SwiftUI I am missing ?
struct ContentView2: View{
#State private var numTouches: Int = 0
#State private var num: String = ""
var body: some View {
VStack{
Button("Touch me pls"){
self.numTouches += 1
}
Text("\(numTouches)")
TextField("Hello enter a number", text: $num)
}.onReceive(Just(num)) { newValue in
if newValue == "" {
self.numTouches = 0
} else {
self.numTouches = Int.init(newValue)!
}
}
}
}
What happens is that when a button is touched, it increases numTouches, which causes the view's body to re-render. .onReceive subscribes to the Just publisher that immediately publishes the value num, which is empty "" in the beginning, and that sets numTouches back to 0.
It sounds that you have really just a single variable, which is being updated from two places:
via TextField
via Button's action
So, keep it as single #State var numTouches: Int:
struct ContentView2: View{
#State private var numTouches: Int = 0
var body: some View {
VStack{
Button("Touch me pls"){
self.numTouches += 1
}
Text("\(numTouches)")
TextField("Hello enter a number",
text: $numTouches, formatter: NumberFormatter()))
// .keyboardType(.numberPad) // uncomment for number pad keyboard
}
}
}

How do you customize the Picker element in swiftui?

I'm trying to create a simple form, and I need a segmented picker. But I can't get the text to wrap and have the longest text elements go on several lines.
Text("QQQ")
Picker("QQQ", selection: pBody) {
ForEach(0..<array.count) { index in
Text(array[index]).tag(index)
}
}
Is this even possible?
not sure what your are after, but if you want a segmented picker, try this:
struct ContentView: View {
#State var array = ["aaa","bbb", "ccc", "ddd", "eee", "fff", "ggg", "hhhhhhh", "iiiiiii", "jjjjjjjjjjjjjj", "kkkkk", "lllll", "mmmmmmmm","nnn","ooooooooo","ppppp"]
#State var pBody = ""
var body: some View {
VStack {
Text("QQQ")
ScrollView (.horizontal) {
Picker("QQQ", selection: $pBody) {
ForEach(0..<array.count) { index in
Text(self.array[index]).tag(index)
}
}.pickerStyle(SegmentedPickerStyle()) // <--- segmented picker
}
}
}