SwiftUI - Index out of range - arrays

I start by adding some integers to an array in onAppear for my outermost stack. But when I try to display the contents of the array using ForEach, I get an index out of range error.
struct MyView: View {
#State private var answers = [Int]()
var body: some View {
VStack {
ForEach(0..<4) { number in
Text("\(answers[number])")
}
}
.onAppear {
for _ in (0..<4) {
answerArray.append(Int.random(in: 1...10))
}
}

Never retrieve items by hard-coded indices in a ForEach expression.
Do count the array, the loop is skipped (safely) if the array is empty.
ForEach(0..<answers.count) { number in
Or - still simpler – enumerate the items rather than the indices
ForEach(answers, id: \.self) { answer in
Text("\(answer)")
}

onAppear is called after MyView loads for the first time, and at that moment, answers is still empty. That's why your program crashes at ForEach(0..<4), because answers's doesn't have 4 elements yet.
ForEach(0..<4) { number in
Text("\(answers[number])") /// answers is still empty.
}
Instead, you should look over answers.indices, so that answers[number] is guaranteed to exist. Make sure to also provide an id (id: \.self) to satisfy ForEach's Identifiable requirement.
struct MyView: View {
#State private var answers = [Int]()
var body: some View {
VStack {
ForEach(answers.indices, id: \.self) { number in
Text("\(answers[number])")
}
}
.onAppear {
for _ in (0..<4) {
answers.append(Int.random(in: 1...10)) /// you probably meant `answers.append`, not `answerArray.append`
}
}
}
}

Related

Fatal error: Index out of range while animating screens

I am running into an error:
Fatal error: Index out of range
I would like to know what is out of range since I have 6 values printed in the console and 6 in my JSON that I created. This is happening when I am navigating to the last item.
(I already tried similar questions/answers from StackoverFlow with no success).
enter image description here
`
import SwiftUI
public struct Stepper: View {
public enum Step: Equatable {
case fixed, animated(duration: Int)
}
#Binding var selected: Int
let steps: [Step]
#State var timer: Timer?
public init(selected: Binding<Int>, steps: [Step]) {
self._selected = selected
self.steps = steps
}
public var body: some View {
HStack {
ForEach(steps.indices) { item in
StepView(step: steps[item], index: item, selected: $selected)
}
}
.padding(.horizontal,16)
.padding(.vertical,12)
.onChange(of: selected,perform: updateTimer)
.onAppear{
updateTimer(newValue: 0)
}
}
func updateTimer(newValue: Int) {
timer?.invalidate()
guard case .animated(let duration) = steps[newValue] else {
return
} **// the app is crashing here in the guard let **
timer = Timer.scheduledTimer(withTimeInterval: Double(duration),
repeats: false,
block: { _ in
if steps.count > selected + 1 {
withAnimation {
selected += 1
}
}
})
}
}
`
I already tried to update the timer with different values (newValue: ) and to pass item instead of newValue with no success.
ForEach isn't a for loop, it's a View that needs to be supplied with Identifiable data (so it can track changes), e.g.
ForEach(steps) { step in
How often and in what order the closure is called depends on what kind of subviews are used in it.
You also need #State var steps and struct Step: Identifiable

"Thread 1: Fatal error: Index out of range" when removing from array with #Binding

I have run into this issue in SwiftUI. I want to be able to remove an item from an Array when the user presses on a button, but I get a "Thread 1: Fatal error: Index out of range" error when I try. This seems to have to do with the fact that IntView takes in a #Binding: if I make num just a regular variable, the code works fine with no errors. Unfortunately, I need to be able to pass in a Binding to the view for my purposes (this is a simplified case), so I am not sure what I need to do so the Binding doesn't cause the bug.
Here is my code:
import SwiftUI
struct IntView: View {
#Binding var num: Int // if I make this "var num: Int", there are no bugs
var body: some View {
Text("\(num)")
}
}
struct ArrayBugView: View {
#State var array = Array(0...10)
var body: some View {
ForEach(array.indices, id: \.self) { num in
IntView(num: $array[num])
Button(action: {
self.array.remove(at: num)
}, label: {
Text("remove")
})
}
}
}
Any help is greatly appreciated!
In your code the ForEach with indicies and id: \.self is a mistake. The ForEach View in SwiftUI isn’t like a traditional for loop. The documentation of ForEach states:
/// It's important that the `id` of a data element doesn't change, unless
/// SwiftUI considers the data element to have been replaced with a new data
/// element that has a new identity.
This means we cannot use indices, enumerated or a new Array in the ForEach. The ForEach must be on the actual array of identifiable items. This is so SwiftUI can track the row Views moving around, which is called structural identity and you can learn about it in Demystify SwiftUI WWDC 2021.
So you have to change your code to something this:
import SwiftUI
struct Item: Identifiable {
let id = UUID()
var num: Int
}
struct IntView: View {
let num: Int
var body: some View {
Text("\(num)")
}
}
struct ArrayView: View {
#State var array: [Item] = [Item(num:0), Item(num:1), Item(num:2)]
var body: some View {
ForEach(array) { item in
IntView(num: item.num)
Button(action: {
if let index = array.firstIndex(where: { $0.id == item.id }) {
array.remoteAt(index)
}
}, label: {
Text("remove")
})
}
}
}

SwiftUI custom nested array ForEach

So i have a nested array of a model:
let models = [[ButtonModel]]
struct ButtonModel: Identifiable {
let id = UUID()
let value: String
let style: ColorStyle
init(_ value: String, _ style: ColorStyle) {
self.value = value
self.style = style
}
}
Then i want to add this as a grid so i have a VStack in which i loop x amount of HStacks with buttons in it.
But because of some reason i get this error:
Cannot convert value of type '[[ButtonModel]]' to expected argument type 'Binding'
Generic parameter 'C' could not be inferred
VStack {
ForEach(viewModel.buttons, id: \.self) { buttons in
HStack(spacing: GridPoints.x2) {
Spacer()
ForEach(buttons) { buttonValue in
if buttonValue == "/" {
imageButton(for: Asset.Image) { viewModel.addValue(buttonValue) }
} else {
Button(buttonValue, action: { viewModel.addValue(buttonValue) })
.buttonStyle(customFont: .h3)
.background(Color.backgroundColor)
.styleText(style: TextStyle.h3)
}
}
Spacer()
}
.padding(GridPoints.x1)
}
}
Anyone know what this error is?
Edit
It looks like you made a mistake when using buttonValue too.
You should use it like this
ForEach(buttons) { button in
if button.value == "/" {
imageButton(for: Asset.Image) { viewModel.addValue(button.value) }
}
...
}
ForEach requires an array whose every element is Identifiable (or telling which one is the id to use using id: \.something).
Your problem is that if you use an array of array, this means that every element of the outer array is another array which is NOT conforming to Identifiable.
If you use ForEach(arrayOfArray, id: \.self), on the other hand, your telling that the identifier will be each element of the outer array that must conform to Hashable. Array conform to Hashable if its element does. So try to change your struct to
struct ButtonModel: Identifiable, Hashable { ... }
Alternate solution
You could change your structure to
struct ButtonsGroup: Identifiable {
let id = UUID()
let buttons: [ButtonModel]
}
Then you will be using it like this:
let groups = [ButtonsGroup]()
ForEach(groups) { group in
ForEach(group.buttons) { button in
Text(button.value)
}
}

Filtering a #Binding array var in a ForEach in SwiftUI returns values based on unfiltered array

I'm a Windows C# developer, new to iOS/SwiftUI development and I think I've worked myself into a hole here.
I have a view with a #Binding variable:
struct DetailView: View {
#Binding var project: Project
The project is an object which contains an array of Tasks. I am looping through the tasks of the project to display its name and a toggle whose state is determined by the Task's variable, isComplete.
ForEach(filteredTasks.indices, id: \.self) { idx in
HStack {
Text(filteredTasks[idx].phase)
.font(.caption)
Spacer()
Text(filteredTasks[idx].name)
Spacer()
Toggle("", isOn: self.$filteredTasks[idx].isComplete)
}
}
}
This took quite a while for me to get to this piece of code, and I found that I had to follow an example with the 'indices' option to get the toggle to work on each Task individually, and to make sure that its isComplete value was saved.
Next, I wanted to filter the list of Tasks based on a Task variable, phase, which has values of Planning, Construction, or Final. So I created 4 buttons (one for each phase, and then an 'All Tasks' to get back to the full, unfiltered list), and after a lot of trial and error (creating filtered arrays that no longer were bound correctly, etc., etc.) I tried this, basically working only with the original array.
List {
ForEach(project.tasks.filter({ $0.phase.contains(filterValue) }).indices, id: \.self) { idx in
HStack {
Text(project.tasks[idx].phase)
.font(.caption)
Spacer()
Text(project.tasks[idx].name)
Spacer()
Toggle("", isOn: self.$project.tasks[idx].isComplete)
}
}
}
And of course, this seemed to work because I can do a test:
func CreateTestArray() {
let testFilterArray = project.tasks.filter({ $0.phase.contains(filterValue) })
}
And that will give me the filtered list I want. However, in my ForEach view, it's not working correctly and I'm not sure how to work around it.
For example, I have 128 tasks, 10 of which have a value of 'Final' and when I use a button setting the filterValue to Final, the testFilterArray actually contains the correct 10 tasks - but in the ForEach view I'm getting the first ten tasks in the original array (which are of the type 'Planning' - the original array is sorted by Planning/Construction/Final); obviously the ForEach, in spite of the filter statement, is working on the original array. The Planning button sends the filterValue = "Planning", and I get the correct results because the filter returns 0-19 indices for the 20 Planning tasks I have in the original array, and since they're first in the original array, it 'appears' that the Planning filter is working correctly, tho in actually it's just by chance that it works, if the array were sorted differently it would not.
Any ideas how I can approach this so that I can actually filter on this array, display the isComplete toggle correctly for each item in the array, as well as update the toggle state dynamically? I feel like I need to start from scratch once again here because I've let these constraints work me into a tiny Swift corner.
Thanks!
Update:
Thank you, #jnpdx, for your quick response - and I should definitely have included objects (which I list below). However, looking back over my object definitions, I wonder if I've made an even more basic error in managing the objects which is why I've gotten boxed in to the situation I have (i.e., in earlier iterations I'd attempted some of your suggestion). At any rate, my published object is 'projects', which is a list of projects I pass to the project list view, and then that view passes a single project to a project view, and then that view lists the tasks in that particular project.
I feel like your answer is pointing me in the right decision, I just need to back back up and look at those object definitions/management and see how to get to a situation where a straightforward solution is possible.
Task:
struct Task: Identifiable, Codable {
let id: UUID
var phase: String
var category: String
var name: String
var isComplete: Bool
init(id: UUID = UUID(), phase: String, category: String, name: String, isComplete: Bool) {
self.id = id
self.phase = phase
self.category = category
self.name = name
self.isComplete = isComplete
}
}
The Project:
struct Project: Identifiable, Codable {
var id: UUID
var name: String
var type: String
var tasks: [Task]
var isComplete: Bool
init(id: UUID = UUID(), name: String, type: String, tasks: [Task] = [], isComplete: Bool) {
self.id = id
self.name = name
self.type = type
self.tasks = tasks
self.isComplete = isComplete
}
}
and the Project model:
class ProjectData: ObservableObject {
// code to access the json file is here
// An accessible list of projects from the saved file
#Published var projects: [Project] = []
// load and save functions follow
Update:
Thanks, #jnpdx, your solution worked after making, as you said I would need to, the tweaks to get it to function within my particular model design. Here are the snippets that finally worked in my case.
In my view:
List {
ForEach(project.tasks.filter({ $0.phase.contains(filterValue) })) { task in
HStack {
Text(task.name)
Toggle("", isOn: self.makeBinding(item: task))
}
}
}
And the called function:
func makeBinding(item: Task) -> Binding<Bool> {
let i = self.project.tasks.firstIndex { $0.id == item.id }!
return .init(
get: { self.project.tasks[i].isComplete },
set: { self.project.tasks[i].isComplete = $0 }
)
}
Let's look at the following line from your code:
ForEach(project.tasks.filter({ $0.phase.contains(filterValue) }).indices, id: \.self) { idx in
In the first part, you filter tasks and then ask for the indices. My suspicion is that you're hoping it would return something like [1, 5, 10, 11, 12], meaning their original positions in the array. But, in reality, you're going to get a contiguous array like [0,1,2,3,4] because it's giving you indices from the newly-created array (the result of filter).
There are a couple of ways to solve this, which also relate to the previous ForEach that you had.
It's more idiomatic to do ForEach and iterate over structs/objects rather than indices. You don't show what Task is made up of, but let's say it's this:
struct Task : Hashable {
var id = UUID()
var name: String
var phrase: String
var isComplete: Bool
}
To iterate on it, you could do:
ForEach(task, id: \.id) { task in
Text(task.name)
Toggle("Done?", isOn: project.taskCompletedBinding(id: task.id)) //explained later
}
I asked about the type of Project in my comment because I'm not totally clear why it's a #Binding. It seems like maybe it's an object? If it's a view model, which would be nice, you can handle your Toggle logic there. Something like:
class Project : ObservableObject {
#Published var tasks : [Task] = [Task(name: "1", phrase: "phase", isComplete: false),Task(name: "2", phrase: "phase", isComplete: true),Task(name: "3", phrase: "phase2", isComplete: false)]
var completedTasks : [Task] {
return tasks.filter { $0.isComplete }
}
func taskCompletedBinding(id: UUID) -> Binding<Bool> {
Binding<Bool>(get: {
self.tasks.first(where: { $0.id == id})?.isComplete ?? false
}, set: { newValue in
self.tasks = self.tasks.map { t in
if t.id == id {
var tCopy = t
tCopy.isComplete = newValue
return tCopy
} else {
return t
}
}
})
}
}
And you can test that it works doing this:
struct ContentView: View {
#ObservedObject var project = Project()
var body: some View {
ForEach(project.tasks, id: \.id) { task in
Text(task.name)
Toggle("Done?", isOn: project.taskCompletedBinding(id: task.id))
}
}
}
If Project is a struct and not an object, it might be good to wrap it in an ObservableObject view model like I did above.

SwiftUI: List, ForEach, indices and .onDelete not working when using TextField() - (Index out of range) [duplicate]

Environment
Xcode 11.2.1 (11B500)
Problem
In order to implement editable teble with TextField on SwiftUI, I used ForEach(0..<items.count) to handle index.
import SwiftUI
struct DummyView: View {
#State var animals: [String] = ["🐢", "🐱"]
var body: some View {
List {
EditButton()
ForEach(0..<animals.count) { i in
TextField("", text: self.$animals[i])
}
}
}
}
However, problems arise if the table is changed to be deleteable.
import SwiftUI
struct DummyView: View {
#State var animals: [String] = ["🐢", "🐱"]
var body: some View {
List {
EditButton()
ForEach(0..<animals.count) { i in
TextField("", text: self.$animals[i]) // Thread 1: Fatal error: Index out of range
}
.onDelete { indexSet in
self.animals.remove(atOffsets: indexSet) // Delete "🐢" from animals
}
}
}
}
Thread 1: Fatal error: Index out of range when delete item
🐢 has been removed from animals and the ForEach loop seems to be running twice, even though animals.count is 1.
(lldb) po animals.count
1
(lldb) po animals
β–Ώ 1 element
- 0 : "🐱"
Please give me advice on the combination of Foreach and TextField.
Thanks.
Ok, the reason is in documentation for used ForEach constructor (as you see range is constant, so ForEach grabs initial range and holds it):
/// Creates an instance that computes views on demand over a *constant*
/// range.
///
/// This instance only reads the initial value of `data` and so it does not
/// need to identify views across updates.
///
/// To compute views on demand over a dynamic range use
/// `ForEach(_:id:content:)`.
public init(_ data: Range<Int>, #ViewBuilder content: #escaping (Int) -> Content)
So the solution would be to provide dynamic container. Below you can find a demo of possible approach.
Full module code
import SwiftUI
struct DummyView: View {
#State var animals: [String] = ["🐢", "🐱"]
var body: some View {
VStack {
HStack {
EditButton()
Button(action: { self.animals.append("Animal \(self.animals.count + 1)") }, label: {Text("Add")})
}
List {
ForEach(animals, id: \.self) { item in
EditorView(container: self.$animals, index: self.animals.firstIndex(of: item)!, text: item)
}
.onDelete { indexSet in
self.animals.remove(atOffsets: indexSet) // Delete "🐢" from animals
}
}
}
}
}
struct EditorView : View {
var container: Binding<[String]>
var index: Int
#State var text: String
var body: some View {
TextField("", text: self.$text, onCommit: {
self.container.wrappedValue[self.index] = self.text
})
}
}
it is because editbutton is IN your list. place it ouside or better in navigationbar.

Resources