my problem is: I have simple array with some Items. I want to display a List with these items using a ForEach with .indices().
(This is because my actual problem handles with Toggle in a List and for the isOn binding I need the index to address a model that is bound to an EnvironmentObject). So the solution to loop over the array items is no possible solution for my problem.
The simplified starting point looks like this:
struct ContentView: View {
#State var items = ["Item1", "Item2", "Item3"]
var body: some View {
List {
ForEach(items.indices) {index in
Text(self.items[index])
}.onDelete(perform: deleteItem)
}
}
func deleteItem(indexSet: IndexSet) {
self.items.remove(atOffsets: indexSet)
}
}
If I now try to swipe-delete a row, I get this error message:
Thread 1: Fatal error: Index out of range
Debugging the index value inside the closure, I can see, that the indices of the items-array does not update. For example: If I delete the first row with "Item 1" and inspect the value of index after deleting the row it returns 2 instead of 0 (which is the expected first index of the array). Why is this and how can I fix this problem?
Thanks for your help!
Just use dynamic content ForEach constructor (_ data: .., id: ...)
ForEach(items.indices, id: \.self) {index in // << here !!
Text(self.items[index])
}.onDelete(perform: deleteItem)
Related
I have a json file where I can display all the contents from it in a view.
struct Test: View{
let quizzs = Bundle.main.decode([Quizz].self, from: "capitals.json")
var body: some View{
ScrollView {
VStack{
ForEach(quizzs!, id: \.self){ item in
Text(item.country)
}
}
}
}
}
But I want to be able to display a country of index[n]
I tried doing this:
#State var index = 0
Text(item.country[index])
But I get this error:
Cannot convert value of type 'Int' to expected argument type 'Range<String.Index>'
How can I make it possible so that I can get a country at index n?
It seems each item has the same country of the array structure. So you can use the index to get the corresponding country.
Try the following ways of writing to meet your needs.
var body: some View {
ScrollView {
VStack {
ForEach(Array(quizzs.enumerated()), id: \.self) { index, item in
Text("\(item.country[index])")
}
}
}
}
}
I would like to create a view which I can pass an array into and have the view edit the array. The following code is a simplified example:
struct Item: Identifiable {
var name: String
var id = UUID()
}
struct EditItems: View {
#Binding var item_list: [Item]
var body: some View {
List {
ForEach(item_list.indices) { idx in
Text(item_list[idx].name)
}
.onDelete(perform: deleteItem)
}
.toolbar {
ToolbarItem(placement: .principal) {
EditButton()
}
}
}
func deleteItem(at offsets: IndexSet) {
item_list.remove(atOffsets: offsets)
}
}
This compiles and runs initially. I can hit "Edit" and delete a list item. After deleting the list item, when I hit "Done", I get "Fatal error: Index out of range". The debugger tells me that my list has 7 items but the line Text(item_list[idx].name) is trying to execute with idx = 7.
So it appears that after deleting the item the ForEach is still running over the old indices instead of the new one shorter indices. Is this because item_list is not #State? When I tried making it both #State and #Binding I got a bunch of errors.
How do I fix this?
The initializer for ForEach that takes in a range can only be used for constant data.
From Apple's docs:
The instance only reads the initial value of the provided data and
doesn’t need to identify views across updates.
Use one of the other ForEach initializers, such as:
ForEach(item_list.enumerated(), id: \.self) { idx, element in
You used constructor of ForEach which creates constant container, use different one, with explicit identifiers, like
List {
ForEach(item_list.indices, id: \.self) { idx in // << here !!
Text(item_list[idx].name)
}
I am trying to alternate at the background color of a List/ForEach using a #State var and toggling it on each repetition. The result is alway that the last color behind the entire List. I have set a breakpoint a Text view inside the ForEach and executing it, I see a stop once per item in the input array, then a display on the screen as expected (i.e. every second row is red and the rest are blue). Then, for some reason, we iterate through the code again, one for each item and leave the loop with the background color of all rows being blue.
The code below is a simplified version of my original problem, which iterates over a Realm Results and is expected to handle a NavigationLink rather than the Text-view and handle deleting items as well.
struct ContentView: View {
let array = ["olle", "kalle", "ville", "valle", "viktor"]
#State var mySwitch = false
var body: some View {
List {
ForEach(array, id: \.self) { name in
Text(name)
.onAppear() {
mySwitch.toggle()
print("\(mySwitch)")
}
.listRowBackground(mySwitch ? Color.blue : Color.red)
}
}
}
}
It because of #State var mySwitch = false variable is attached with all row of ForEach so whenever your mySwitch var change it will affect your all row.
So if you want to make some alternative way, you can use the index of item and check whether your number is even or not and do your stuff according to them.
Demo:
struct ContentView: View {
let array = ["olle", "kalle", "ville", "valle", "viktor"]
#State var mySwitch = false
var body: some View {
List {
ForEach(array.indices, id: \.self) { index in
Text(array[index])
.onAppear() {
mySwitch.toggle()
print("\(mySwitch)")
}
.listRowBackground(index.isMultiple(of: 2) ? Color.blue : Color.red)
}
}
}
}
Goal: Multiple text views visually separated much like what Section{} offers, while also being able to rearrange the items in the list during edit mode. (I am not 100% set on only using section but I haven't found a way to visually distinguish with Form or List.)
The issue: The app crashes on the rearrange when using Section{}.
Error Message: 'Invalid update: invalid number of rows in section 2. The number of rows contained in an existing section after the update (1) must be equal to the number of rows contained in that section before the update (0), plus or minus the number of rows inserted or deleted from that section (0 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).'
Code:
struct SingleItem: Identifiable {
let id = UUID()
let item: String
}
class ItemGroup: ObservableObject{
#Published var group = [SingleItem]()
}
struct ContentView: View {
#ObservedObject var items = ItemGroup()
#State private var editMode = EditMode.inactive
var body: some View {
NavigationView{
Form {
Button("Add Item"){
addButton()
}
ForEach(Array(items.group.enumerated()), id: \.element.id) { index, item in
Section{
Text(items.group[index].item)
}
}
.onMove(perform: onMove)
}
.navigationBarTitle("List")
.navigationBarItems(trailing: EditButton())
.environment(\.editMode, $editMode)
}
}
func addButton() {
let newItem = SingleItem(item: "Word - \(items.group.count)")
self.items.group.append(newItem)
}
private func onMove(source: IndexSet, destination: Int) {
items.group.move(fromOffsets: source, toOffset: destination)
}
}
Use instead .indices. Tested as worked with no crash on Xcode 12 / iOS 14.
Form {
ForEach(items.indices, id: \.self) { i in
Section {
Text(items[i].title)
}
}
.onMove(perform: onMove)
}
I have the following component that renders a grid of semi transparent characters:
var body: some View {
VStack{
Text("\(self.settings.numRows) x \(self.settings.numColumns)")
ForEach(0..<self.settings.numRows){ i in
Spacer()
HStack{
ForEach(0..<self.settings.numColumns){ j in
Spacer()
// why do I get an error when I try to multiply i * j
self.getSymbol(index:j)
Spacer()
}
}
Spacer()
}
}
}
settings is an EnvironmentObject
Whenever settings is updated the Text in the outermost VStack is correctly updated. However, the rest of the view is not updated (Grid has same dimenstions as before). Why is this?
Second question:
Why is it not possible to access the i in the inner ForEach-loop and pass it as a argument to the function?
I get an error at the outer ForEach-loop:
Generic parameter 'Data' could not be inferred
TL;DR
Your ForEach needs id: \.self added after your range.
Explanation
ForEach has several initializers. You are using
init(_ data: Range<Int>, #ViewBuilder content: #escaping (Int) -> Content)
where data must be a constant.
If your range may change (e.g. you are adding or removing items from an array, which will change the upper bound), then you need to use
init(_ data: Data, id: KeyPath<Data.Element, ID>, content: #escaping (Data.Element) -> Content)
You supply a keypath to the id parameter, which uniquely identifies each element that ForEach loops over. In the case of a Range<Int>, the element you are looping over is an Int specifying the array index, which is unique. Therefore you can simply use the \.self keypath to have the ForEach identify each index element by its own value.
Here is what it looks like in practice:
struct ContentView: View {
#State var array = [1, 2, 3]
var body: some View {
VStack {
Button("Add") {
self.array.append(self.array.last! + 1)
}
// this is the key part v--------v
ForEach(0..<array.count, id: \.self) { index in
Text("\(index): \(self.array[index])")
//Note: If you want more than one views here, you need a VStack or some container, or will throw errors
}
}
}
}
If you run that, you'll see that as you press the button to add items to the array, they will appear in the VStack automatically. If you remove "id: \.self", you'll see your original error:
`ForEach(_:content:)` should only be used for *constant* data.
Instead conform data to `Identifiable` or use `ForEach(_:id:content:)`
and provide an explicit `id`!"
ForEach should only be used for constant data. So it is only evaluated once by definition. Try wrapping it in a List and you will see errors being logged like:
ForEach, Int, TupleView<(Spacer, HStack, Int, TupleView<(Spacer, Text, Spacer)>>>, Spacer)>> count (7) != its initial count (0). ForEach(_:content:) should only be used for constant data. Instead conform data to Identifiable or use ForEach(_:id:content:) and provide an explicit id!
I was surprised by this as well, and unable to find any official documentation about this limitation.
As for why it is not possible for you to access the i in the inner ForEach-loop, I think you probably have a misleading compiler error on your hands, related to something else in the code that is missing in your snippets. It did compile for me after completing the missing parts with a best guess (Xcode 11.1, Mac OS 10.14.4).
Here is what I came up with to make your ForEach go over something Identifiable:
struct SettingsElement: Identifiable {
var id: Int { value }
let value: Int
init(_ i: Int) { value = i }
}
class Settings: ObservableObject {
#Published var rows = [SettingsElement(0),SettingsElement(1),SettingsElement(2)]
#Published var columns = [SettingsElement(0),SettingsElement(1),SettingsElement(2)]
}
struct ContentView: View {
#EnvironmentObject var settings: Settings
func getSymbol(index: Int) -> Text { Text("\(index)") }
var body: some View {
VStack{
Text("\(self.settings.rows.count) x \(self.settings.columns.count)")
ForEach(self.settings.rows) { i in
VStack {
HStack {
ForEach(self.settings.columns) { j in
Text("\(i.value) \(j.value)")
}
}
}
}
}
}
}