SwiftUI ForEach open vs closed range - swiftui

I'd like to understand ForEach loop and ranges. Here is the code:
This is valid:
ForEach(0..<5) {
Text("Number: \($0)")
}
This throws exception Generic parameter 'ID' could not be inferred:
ForEach(0...4) {
Text("Number: \($0)")
}
I can fix it by specifying id:
ForEach(0...4, id: \.self) {
Text("Number: \($0)")
}
I was wondering why the first range 0..<5 is Identifiable and the 0..4 is not. They are the same type Range<Int>. I just don't understand.
Many thanks!

Reason being there is no initializer for the ClosedRange<Int>. Refer to this link for the supported ones.

Related

How to iterate over an array in a LazyVGrid

I am trying to iterate over an array of objects inside a LazyVGrid so as to print out the first name of each contact into each cell. The issue is, I am getting error: "Type '()' cannot conform to 'View'" on LazyVGrid...
After looking up potential fixes, I am seeing that some are saying we can't use functional code in a grid, but apple is using functional code in their documentation on how to use a LazyVGrid, so I am not understanding how this is true.
Does anyone know what I am missing?
Please see below code:
var body: some View {
NavigationStack {
VStack {
ScrollView {
LazyVGrid(columns: threeColumnGrid) {
// employeeContactsTempData is simply an array of objects containing contact related data
employeeContactsTempData.forEach({ contact in
VStack {
Text(contact.firstName)
}
})
}
}
Spacer()
}.background(.blue)
}
}
You should use ForEach to iterate over an array to create views.
LazyVGrid(columns: threeColumnGrid) {
// Use id argument if your contact model is not identifiable.
ForEach(employeeContactsTempData, id: \.self) { contact in
VStack {
Text(contact.firstName)
}
}
}

Why is scrollTo working on Int and not Message custom object type?

I tried the same code with an Int instead of messages, it worked.
When I try and use the message object, for some reason it's not liking it. Even though the .id is UUID().
The messages appear in scrollView and can be scrolled manually although it's not scrolling automatically to the bottom once a message is added.
I've tried .self instead of .id in the ForEach, flipping the order of ScrollView and ScrollViewReader, not using a .id on MessageRow, downcast to UUID inside scrollTo, nothing seems to be working...
What am I missing?
Thank you in advance!
ScrollViewReader { scrollView in
ScrollView(.vertical) {
LazyVStack {
ForEach(groupViewModel.messages) { message in
MessageRowView(message: message)
.padding(.leading)
}
}
.onAppear {
if !groupViewModel.messages.isEmpty {
if let last = groupViewModel.messages.last {
scrollView.scrollTo(last)
}
}
}
}
}
struct Message: Codable, Equatable, Identifiable, Hashable {
let id: UUID
let sender: FSUser
let message: String
let sendDate: Date
}
Welcome to Stack Overflow! You are missing the simple fact that a message.id != message?.id. Your .id(message.id) is a non-optional UUID, however, groupViewModel.messages.last?.id is an optional. Since they don't match, you won't get the scroll. I will give you one better. First, you don't need to use message.id. You can simply compare the messages themselves as Identifiable conforms to Equatable. So, your code becomes:
ScrollViewReader { scrollView in
ScrollView(.vertical) {
LazyVStack {
// You can also skip id: \.id as message is Identifiable
ForEach(groupViewModel.messages) { message in
MessageRowView(message: message)
.id(message)
}
}
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now()) {
if !groupViewModel.messages.isEmpty {
// Use if let to unwrap the optional
if let last = groupViewModel.messages.last {
// Use the unwrapped value here
scrollView.scrollTo(last)
}
}
}
}
}
}
If you haven't stated conformance of your struct to Identifiable, just do it. I am assuming you did, but you did not post that part of the code so we don't have a Minimal, Reproducible Example (MRE). Most questions will need an MRE to be answered, so make sure you understand what that is.
Edit:
In looking at your struct, try this:
struct Message: Codable, Equatable, Identifiable, Hashable {
let id = UUID()
let sender: FSUser
let message: String
let sendDate: Date
}
You can set the UUID immediately as it will never change. Also, not that it matters in the long run, but if you conform to Identifiable you get 'Equatable' conformance.

Indices don't update when deleting item from list

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)
}

Swift Ui - Different View(Layout) for first item of a List

I am relatively new to Swift. I want to accomplish that the first item of the weatherList is displayed with a different View.
Unfortunately I get this error in those 2 Lines (VStack and at ForEach-Line). If i put away the if i == 0 then and just use for normal layout for all listitems there is no error.
ERROR: Type '()' cannot conform to 'View'; only struct/enum/class types can conform to protocols
Here is my code:
var body: some View {
NavigationView {
VStack { // here first error
if self.model.error != nil {
Text(self.model.error!)
}
ForEach(0..<self.model.weatherList.count, id: \.self) { i in // here second error
if i == 0 {
NavigationLink(destination: DetailView(model: self.model.weatherList[i])) {
RowView(model: self.model.weatherList[i])
}
}
else{
NavigationLink(destination: DetailView(model: self.model.weatherList[i])) {
RowView(model: self.model.weatherList[i])
}
}
}
}
.navigationBarTitle(Text("Weather in " + UserSettings.instance.locationSetting))
.navigationBarItems(trailing:
Button(action: {
self.model.reload()
}) {
Image(systemName: "arrow.clockwise")
}
.disabled(model.reloading)
)
}
}
Would appreciate each advice. Thanks.
I haven't tries this, but what if you wrap the NavigationLink like this:
if i == 0 {
AnyView(
NavigationLink(destination: DetailView(model: self.model.weatherList[i])) {
RowView(model: self.model.weatherList[i])
})
} else {
AnyView(
NavigationLink(destination: DetailView(model: self.model.weatherList[i])) {
RowView(model: self.model.weatherList[i])
})
}
It's a little tricky to know for sure without having the rest of your code to make this compile, but I mocked it up locally and it looks like all you need to change is:
ForEach(0..<self.model.weatherList.count, id: \.self) { i in
to
ForEach(0..<self.model.weatherList.count) { i in // remove the id: argument
Explanation:
If you're using a range to iterate through your array, you don't use the id: argument. The range is automatically identifiable. The ForEach(_:id:content:) initialiser is used for cases where you might say
ForEach(self.model.weatherlist, id: \.self) { model in
SomeView(model: model)
}
Does that compile for you?
Edit: The example you've given looks like it will do the same thing whether the index is 0 or not. Is there a reason why it doesn't work if you try:
ForEach(0..<self.model.weatherList.count) { i in
NavigationLink(destination: DetailView(model: self.model.weatherList[i])) {
RowView(model: self.model.weatherList[i])
}
}

View is not rerendered in Nested ForEach loop

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)")
}
}
}
}
}
}
}