iOS16 SwiftUI app crashes when trying to create view using loop - swift

I've defined a view in SwiftUI which takes an Int array and is supposed to display the elements of the array in a VStack such that every "full row" contains three elements of the array and then the last "row" contains the remainder of elements. When running the app on iOS16 I get "Fatal error: Can't remove first element from an empty collection" for the call let die = dice.removeFirst() (also when passing in a non-empty array of course). I've tried following the debugger but I don't understand the way it jumps around through the loops.
On iOS15 this code worked fine. In the actual program I don't display the array content as Text but I have images associated with each Int between 1 and 6 which I display. I replaced this with Text for simplicity's sake.
Thanks for any help!
struct DiceOnTableView: View {
let diceArray: [Int]
var body: some View {
let fullRows: Int = diceArray.count / 3
let diceInLastRow: Int = diceArray.count % 3
var dice: [Int] = diceArray
VStack {
ForEach(0..<fullRows, id: \.self) { row in
HStack {
ForEach(0..<3) { column in
let die = dice.removeFirst()
Text("\(die)")
}
}
}
HStack {
ForEach(0..<diceInLastRow, id: \.self) { column in
let die = dice.removeFirst()
Text("\(die)")
}
}
}
}
}

This does kind of work on iOS 15 (but strangely - the order of the dice is unexpected), and crashes on iOS 16. In general, you should not be using vars in SwiftUI view building code.
Your code can be modified to compute the index into the original diceArray from the row, fullRows, and column values:
struct DiceOnTableView: View {
let diceArray: [Int]
var body: some View {
let fullRows: Int = diceArray.count / 3
let diceInLastRow: Int = diceArray.count % 3
VStack {
ForEach(0..<fullRows, id: \.self) { row in
HStack {
ForEach(0..<3) { column in
Text("\(diceArray[row * 3 + column])")
}
}
}
HStack {
ForEach(0..<diceInLastRow, id: \.self) { column in
Text("\(diceArray[fullRows * 3 + column])")
}
}
}
}
}

The ForEach View will crash if you use it like a for loop with dynamic number of items. You need to supply it an array of item structs with ids, e.g. one that is Identifiable, e.g.
struct DiceItem: Identifable {
let id = UUID()
var number: Int
}
#State var diceItems: [DiceItem] = []
ForEach(diceItems) { diceItem in
And perhaps use a Grid instead of stacks.

Related

Recursive SwiftUI views

I'm unable to display a SwiftUI view that calls itself in a ForEach loop for some reason. The app hangs and then crashes when it tries to display this view:
struct LoopView: View {
let loop: Loop
var body: some View {
HStack {
Text(String(loop.multiplier))
.font(.title)
Text("x")
VStack {
// ### THE PROBLEM IS HERE ###
if let loops = loop.loops {
ForEach(loops, id: \.id) { innerLoop in
// Text(String(innerLoop.multiplier)) // << This works
LoopView(loop: innerLoop) // << This causes the system to hang
}
} else {
// This stuff here is fine
if let components = loop.components {
ForEach(components, id: \.id) { component in
ComponentView(component: component)
}
}
}
}
}
}
}
struct LoopView_Previews: PreviewProvider {
static let moc = DataController.preview.container.viewContext
static var previews: some View {
let outterLoop = Loop(context: moc)
outterLoop.id = UUID()
outterLoop.multiplier = 5
outterLoop.addToInternalLoops(DataController.exampleLoop())
// vvv Does not change outcome if commented out vvv
outterLoop.addToInternalLoops(DataController.exampleLoop())
return LoopView(loop: outterLoop)
}
}
As per the comments, I can access the attributes of the looped item just fine, meaning there's nothing wrong with the loop and the elements can be accessed. However, I'm unable to use my loop recursively. innerLoop doesn't contain a loop meaning it will stop recursing after going only one level deep.
I'm using Xcode 13.2.1. Thank you for your help!
Did some more debugging and found the error came from my self-referencing one-to-many entity needed an inverse reference to itself. See this post for details.

Adapting #State variables and UI to user actions in SwiftUI

I am a novice at programming and exploring SwiftUI. I've been tackling a challenge for too long, and hoping that someone can guide me to the right direction!
I want a list of interlinked sliders (as in Interlinked Multiple Sliders in SwiftUI), but with the number of sliders that change dynamically, depending on actions taken by a user.
For example, a user can choose various items, and later on adjust the percentage variable with sliders (and where these percentages are interdependent as in the linked example).
class Items: ObservableObject {
#Published var components = [ItemComponent]()
func add(component: itemComponent){
components.append(component)
}
}
struct ItemComponent: Hashable, Equatable, Identifiable {
var id = UUID().uuidString
var name: String = ""
var percentage: Double
}
Conceptually, it seems I need to do two things to adapt the linked code:
generate an array of Binding with the number of elements equal to Items.Component.EndIndex and
assign each Binding to the percentage of each ItemComponent.
I am fumbling on both. For 1., I can easily manually create any number of variables, e.g.
#State var value1 = 100
#State var value2 = 100
#State var value3 = 100
let allBindings = [$value1, $value2, $value3]
but how do I generate them automatically?
For 2., I can use ForEach() to call the components, or Index, but not both together:
ForEach(Items.components){ component in
Text("\(component.name)")
Text("\(component.percentage)")
}
ForEach(Items.components.indices){ i in
synchronizedSlider(from: allBindings, index: i+1)
}
In broken code, what I want is something like:
ForEach(Items.component){component in
HStack{
Text("component.name")
Spacer()
synchronizedSlider(from: allBindings[$component.percentage], index: component.indexPosition)
}
where allBindings[$component.percentage] is a binding array comprised of each itemComponent's percentage, and the index is an itemComponent's index.
I am happy to share more code if relevant. Any help would be greatly appreciated!
To adapt the existing code you linked, if you're going to have a dynamic number of sliders, you'll definitely want your #State to be an array, rather than individual #State variables, which would have to be hard coded.
Once you have that, there are some minor syntax issues changing the synchronizedBinding functions to accept Binding<[ItemComponent]> rather than [Binding<Double>], but they are pretty minor. Luckily, the existing code is pretty robust outside of the initial hard-coded states, so there isn't any additional math to do with the calculations.
I'm using ItemComponent rather than just Double because your sample code included it and having a model with a unique id makes the ForEach code I'm using for the sliders easier to deal with, since it expects uniquely-identifiable items.
struct ItemComponent: Hashable, Equatable, Identifiable {
var id = UUID().uuidString
var name: String = ""
var percentage: Double
}
struct Sliders: View {
#State var values : [ItemComponent] = [.init(name: "First", percentage: 100.0),.init(name: "Second", percentage: 0.0),.init(name: "Third", percentage: 0.0),.init(name:"Fourth", percentage: 0.0),]
var body: some View {
VStack {
Button(action: {
// Manually setting the values does not change the values such
// that they sum to 100. Use separate algorithm for this
self.values[0].percentage = 40
self.values[1].percentage = 60
}) {
Text("Test")
}
Button(action: {
self.values.append(ItemComponent(percentage: 0.0))
}) {
Text("Add slider")
}
Divider()
ScrollView {
ForEach(Array(values.enumerated()),id: \.1.id) { (index,value) in
Text(value.name)
Text("\(value.percentage)")
synchronizedSlider(from: $values, index: index)
}
}
}.padding()
}
func synchronizedSlider(from bindings: Binding<[ItemComponent]>, index: Int) -> some View {
return Slider(value: synchronizedBinding(from: bindings, index: index),
in: 0...100)
}
func synchronizedBinding(from bindings: Binding<[ItemComponent]>, index: Int) -> Binding<Double> {
return Binding(get: {
return bindings[index].wrappedValue.percentage
}, set: { newValue in
let sum = bindings.wrappedValue.indices.lazy.filter{ $0 != index }.map{ bindings[$0].wrappedValue.percentage }.reduce(0.0, +)
// Use the 'sum' below if you initially provide values which sum to 100
// and if you do not set the state in code (e.g. click the button)
//let sum = 100.0 - bindings[index].wrappedValue
let remaining = 100.0 - newValue
if sum != 0.0 {
for i in bindings.wrappedValue.indices {
if i != index {
bindings.wrappedValue[i].percentage = bindings.wrappedValue[i].percentage * remaining / sum
}
}
} else {
// handle 0 sum
let newOtherValue = remaining / Double(bindings.wrappedValue.count - 1)
for i in bindings.wrappedValue.indices {
if i != index {
bindings[i].wrappedValue.percentage = newOtherValue
}
}
}
bindings[index].wrappedValue.percentage = newValue
})
}
}

How to build NumberField in SwiftUI?

I'm working on MacOS and I try to make NumberField — something like TextField, but for numbers. In rather big tree of views at the top I had:
...
VStack {
ForEach(instances.indices, id:\.self) {index in
TextField("",
text: Binding(
get: {"\(String(format: "%.1f", instances[index].values[valueIndex]))"},
set: {setValueForInstance(index, valueIndex, $0)})
)
}
}
...
And it worked well, but not nice:
✔︎ when I changed value, all View structure was redrawn – good
✔︎ values was updated if they were changed by another part of Views structure – good
✖︎ it was updated after each keypresses, which was annoying, when I tried to input 1.2, just after pressing 1 view was updated to 1.0. Possible to input every number but inconvenient – bad
So, I tried to build NumberField.
var format = "%.1f"
struct NumberField : View {
#Binding var number: Double {
didSet {
stringNumber = String(format: format, number)
}
}
#State var stringNumber: String
var body: some View {
TextField("" , text: $stringNumber, onCommit: {
print ("Commiting")
if let v = Double(stringNumber) {
number = v
} else {
stringNumber = String(format:format, number)
}
print ("\(stringNumber) , \(number)" )
})
}
init (number: Binding<Double>) {
self._number = number
self._stringNumber = State(wrappedValue: String(format:format, number.wrappedValue))
}
}
And It's called from the same place as before:
...
VStack {
ForEach(instances.indices, id:\.self) {index in
NumberField($instances[index].values[valueIndex])
}
}
...
But in this case it never updates NumberField View if values was changed by another part of View. Whats's wrong? Where is a trick?

Dynamic TextFields based on user input using SwiftUI

I am trying to create a dynamic set of TextFields which are added after the user presses the add button. Each press will add another set of those fields. I am new to this so please bear with me. I am getting a fatal error: index out of range. Here is a simple example of what I am trying to achieve.
struct ContentView: View {
#State var name: [String] = []
#State var counter = 0
var body: some View {
Form {
Section {
ForEach(0..<counter, id: \.self) { index in
TextField("Name", text: self.$name[index])
}
Button(action:{
self.counter += 1
}) {
Text("Add more")
}
}
}
}
}
You're increasing the counter without adding new items. If you add a new item to your array it will work without errors:
Button(action:{
self.name.append("")
self.counter += 1
}) {
Text("Add more")
}
But preferably don't use the counter variable at all. If you add a new item to the names array it's count will automatically increase. You can use it in the ForEach loop like this:
ForEach(0..<names.count, id: \.self) { index in
TextField("Name", text: self.$names[index])
}
Button(action:{
self.names.append("")
}) {
Text("Add more")
}
Note: For arrays it's better to use plural names: names instead of name. It indicates it's a collection of items.
SWIFTUI:
Here's the code to show multiple dynamic Text() element:
#State var textsArray = ["a","b","c","d"]
HStack{ ForEach(textsArray, id: \.self)
{ text in
Text("\(text)")
}
}
You can add more texts into "textsArray" or you can change the values in "textsArray" and it'll be automatically changing on UI.

Problem with code which worked in Xcode11 beta 4 but stop working in beta 5

I had a View which should render a GridView in the beta 4 everything worked great but in the beta 5 of Xcode 11 and beta 5 of macOS Catalina it stoped working.
struct List : View {
var rows: [[Int]]
var spacing: CGFloat = (screen.width-330)/4
var list: [ReminderModel]
var number: Int
var body: some View {
return VStack {
ForEach(rows, id: \.self) { row in
HStack(spacing: self.spacing) { //The error is at this bracket
ForEach(row) { item in
Reminder(closed: self.list[item].closed, text: self.list[item].text)
self.number % 3 == 0 ? nil : VStack() {
self.number-1 == item ? AddReminder() : nil
}
}
Spacer()
}.padding(.top, self.spacing).padding(.leading, self.spacing)
}
if self.number % 3 == 0 {
HStack() {
AddReminder().padding(.leading, self.spacing).padding(.top, self.spacing)
Spacer()
}
}
}
}
}
Error:
Unable to infer complex closure return type; add explicit type to disambiguate
Update 1:
I found that the problem is this part of the code:
self.number % 3 == 0 ? nil : VStack() {
self.number-1 == item ? AddReminder() : nil
}
I also tried this but also didn't work:
if (self.number % 3 != 0 && self.number-1 == item) {
AddReminder()
}
I simplified your code down into something I could run:
struct ContentView: View {
var rows: [[Int]] = [[0, 1, 2], [3, 4, 5]]
var body: some View {
VStack {
ForEach(rows, id: \.self) { row in
HStack {
ForEach(row) { item in
EmptyView()
}
}
}
}
}
}
...and I got this error:
Referencing initializer 'init(_:content:)' on 'ForEach' requires that 'Int' conform to 'Identifiable'
I'm guessing that in previous betas Int conformed to Identifiable and that beta 5 changed that. So to fix this, just change your second ForEach to ForEach(row, id: \.self).
Update
After removing the parts of your code that I could not run, I managed to get the same error.
Error: Unable to infer complex closure return type; add explicit type to disambiguate
It appears that ForEach is expecting one view to be returned from its body, not multiple as you have here:
ForEach(row) { item in
Reminder(closed: self.list[item].closed, text: self.list[item].text)
self.number % 3 == 0 ? nil : VStack() {
self.number-1 == item ? AddReminder() : nil
}
}
You're trying to return both a Reminder and an optional VStack, so the compiler can't determine what the return type is supposed to be. This may have worked in the past because ForEach could previously handle tuple views and no longer does - I'm not sure. Whatever the case, you need to first change the ForEach to ForEach(row, id: \.self) as I pointed out earlier, and then you have to wrap everything inside the ForEach in a group, like this:
ForEach(row, id: \.self) { item in
Group {
Reminder(closed: self.list[item].closed, text: self.list[item].text)
self.number % 3 == 0 ? nil : VStack {
self.number - 1 == item ? AddReminder() : nil
}
}
}
One last thing that I've just noticed. The name of your struct should not be List. List already exists in SwiftUI, and you shouldn't name your custom views in ways that will conflict with framework defined types. I would suggest that you rename your view to ReminderList if that adequately describes its purpose.