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)
}
Related
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)
}
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)
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)")
}
}
}
}
}
}
}
I'm trying to create a simple dynamic list grouped into sections. (SwiftUI iOS13 Xcode11 beta 2)
A simple static example would be :
struct StaticListView : View {
var body: some View {
List {
Section(header: Text("Numbers"), footer: Text("...footer...")) {
Text("1")
Text("2")
Text("3")
}
Section(header: Text("Letters"), footer: Text("...footer...")) {
Text("a")
Text("b")
Text("c")
}
}
}
}
This displays as expected a nice list with section headers and footers
But when I try to do this from a dynamic list like this :
struct TestData: Identifiable {
var id = UUID()
var title: String
var items: [String]
}
struct ListView : View {
let mygroups = [
TestData(title: "Numbers", items: ["1","2","3"]),
TestData(title: "Letters", items: ["A","B","C"]),
TestData(title: "Symbols", items: ["€","%","&"])
]
var body: some View {
List (mygroups) { gr in
Section(header: Text(gr.title),
footer: Text("...footer...") ) {
ForEach(gr.items.identified(by: \.self)) { item in
Text(item)
}
}
}
}
}
The result is a list with only 3 rows. Both the Section header, all the content cells and the footer are combined horizontally into a single row.
What am I missing?
Giving List a set of items seems to make it incorrectly treat Section as a single view.
You should probably file a radar for this, but in the meantime, this will give you the behavior you're looking for:
struct ListView : View {
let mygroups = [
TestData(title: "Numbers", items: ["1","2","3"]),
TestData(title: "Letters", items: ["A","B","C"]),
TestData(title: "Symbols", items: ["€","%","&"])
]
var body: some View {
List {
ForEach(mygroups) { gr in
Section(header: Text(gr.title),
footer: Text("...footer...") ) {
ForEach(gr.items.identified(by: \.self)) { item in
Text(item)
}
}
}
}
}
}
Just a small fix for a correct answer above. Since
ForEach(gr.items.identified(by: \.self)) { item in
Text(item)
}
does not compile as for me, so this do compile and works like a charm:
ForEach(gr.items, id: \.self, content: { item in
Text(item)
})
While the above solutions work for static data, I'm running into a different situation. In my case, the "mygroups" equivalent is empty when the List is first composed. In the .onAppear{} block, I build the groups.
Once the groups are built I update the #State and the List crashes with this old friend message:
'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of sections. The number of sections contained in the table view after the update (2) must be equal to the number of sections contained in the table view before the update (2), plus or minus the number of sections inserted or deleted (2 inserted, 0 deleted).'
I went from an empty array to one with two sections. I don't think the List is ready yet for such complex dynamic changes (unless there's an API I haven't found yet).
What I'm likely to do is try and build this before the List gets a chance to see it.
Embedding the List inside a VStack solved the issue for me.