Text view is not show in LazyVGrid - swiftui

I experience kind of weird behavior of placing Text view in nested ForEach in one LazyVGrid view. My code look like this
LazyVGrid(columns: dataColumns) {
ForEach(months, id: \.self) { month in
Text("\(dateFormatter.string(from: month))")
}
ForEach(categories) { category in
ForEach(months, id: \.self) { month in
Text("aaa")
}
}
}
and "aaa" string is not shown but if I add just another Text view like this
ForEach(months, id: \.self) { month in
Text("aaa")
Text("bbb")
}
then both Text views with strings are repeatedly shown as expected. Any idea? Thanks.

The issue here is to do with view identity. LazyVGrid uses each views identity to know when they need to be loaded in.
Structural identity is also used by SwiftUI to identify views based on the structure of their view hierarchy. This is why when you added another view to the ForEach instance the views show, as they now are uniquely structurally identifiable from the other ForEach content.
Here, you have two separate ForEach instances inside the LazyVGrid which use the same identifiers. Two of the forEach instances are using months with the id \.self to identify them. These are conflicting and should be unique to avoid these sorts of issues.
To fix this you should preferably use a unique array of elements for each ForEach instance, though could create a copy of the months array to use in the second ForEach instead of referring to the same instances. Here's an example:
import SwiftUI
import MapKit
struct MyType: Identifiable, Hashable {
var id = UUID()
var name = "Tester"
}
struct ContentView: View {
let columns = [GridItem(.flexible()), GridItem(.flexible())]
var months: [MyType] = []
var monthsCopy: [MyType] = []
private var monthValues: [MyType] {
[MyType()]
}
init() {
months = monthValues
monthsCopy = monthValues
}
var body: some View {
LazyVGrid(columns: columns) {
ForEach(months) { _ in
Text("Test1")
Text("Test2")
}
ForEach(monthsCopy) { _ in
Text("Test3")
Text("Test4")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
If you replace ForEach(monthsCopy) with ForEach(months) you will see the issue you were faced with.

Related

Strange results with data input and nested Foreach loops

I use Xcode 14.2, macOS Monterey 12.6.2 and Minimum Deployments 15.5.
I want to display 4 TextFields in a 2x2 grid for integer input.
The data is stored in an extern struct Model as an ObservableObject.
The environment variable is injected in the #main struct.
The TextField must be equipped with an .id modifier. Otherwise the following error message appears:
LazyVGridLayout: the ID 0 is used by multiple child views, this will give undefined results!
LazyVGridLayout: the ID 1 is used by multiple child views, this will give undefined results!
Adding .id(row + col) results in a single error message:
LazyVGridLayout: the ID 1 is used by multiple child views, this will give undefined results!
Using UUID() for generating an ID .id(UUID()) results in a strange effect. Trying to enter a multi digit value in a Textfeld fails. The software keyboard vanishes after the input of the first digit.
Changing the id to .id(row + 7 * col) results in the expected behavior of the demo App. However, this "solution" shouldn't be the right way to solve the problem.
Has somebody an idea was is going wrong?
struct ContentView: View {
let colums = [GridItem(),GridItem()]
#EnvironmentObject var model: Model
var body: some View {
List {
LazyVGrid(columns: colums) {
ForEach(0...1, id:\.self) { row in
ForEach(0...1, id: \.self) { col in
TextField("", value: $model.rows[row].values[col], format: .number)
.id(UUID())
// .id(row + 7 * col)
}
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
class Model: ObservableObject {
#Published
var rows = [Values(), Values()]
struct Values {
var values = [0, 0]
}
}
#main
struct KeyboardFocusApp: App {
#StateObject var model = Model()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(model)
}
}
}
Following my comment please let me suggest a different approach. The whole fun of VGrid is that you don't need the grid structure in your data. You define the column and just throw the data in.
Also as commented it would be preferable to have the model data itself identifiable.
struct ContentView: View {
// #EnvironmentObject var model: Model
#StateObject var model = Model()
let colums = [GridItem(),GridItem()]
var body: some View {
List {
LazyVGrid(columns: colums) {
// no need for .id, as items are identifiable
// $ init makes $item modifiable
ForEach($model.items) { $item in
TextField("", value: $item.value, format: .number)
}
}
}
}
}
class Model: ObservableObject {
init() { // initialize with 4 items
items = []
for _ in 0..<4 {
items.append(Item())
}
}
#Published var items: [Item]
struct Item: Identifiable {
let id = UUID()
var value = 0
}
}

SwiftUI: Using List and ForEach to display model data of different types with an array of Strings

I want to display a dynamic list of items that come from model data. The problem I have is how to properly use ForEach to display a var that is an array of Strings. There are no compiling issues with the code, but I cannot get the List to display.
Here is the struct for the model data:
struct OrderInfo: Codable, Identifiable, Hashable {
var id: Int
var orderNumber: String
var activeOrderStatus: Bool
var pickupSection: Int
var pickupStand: String
var pickupItems: [String]
}
Here is the code for my View:
struct CompletedOrderPickUp: View {
var completedOrder: OrderInfo
var body: some View {
ScrollView {
VStack {
HStack {
Text("Section")
Spacer()
Text("\(completedOrder.pickupSection)")
}
.pickUpDetailsTitleModifier()
HStack {
Text("Stand Name")
Spacer()
Text(completedOrder.pickupStand)
}
.pickUpDetailsTitleModifier()
HStack {
Text("Order Items")
Spacer()
}
.pickUpDetailsTitleModifier()
List {
ForEach(completedOrder.pickupItems, id: \.self) { items in
Text("\(items)")
Text(items)
}
}
}
}
}
}
And here is the screenshot of what it produces:
Screenshot with no items under "Order Items"
There's no issue with accessing the two other variables (pickupSection and pickupStand), which leads me to believe the problem lies with how to properly access the array of Strings within the data for pickupItems.
My friend Google has lots to say about ForEach and accessing data but not a lot about the combination of the two. Please let me know if I may clarify my question. Any help is much appreciated!

no exact matches in call to initializer.(I do not know how to fix it)

struct ContentView: View {
var countries = ["dubai","Dutch","Finland","france","france","Fuji","India","Intaly","Japan","Korean","nepal","pakistan","philippe","Rusia","swiss","Tailand"].shuffled()
var body: some View {
VStack {
ForEach(0...2) { number in Image(self.countries[number])(error here:no exact matches in call to initializer.)
.border(Color.black,width:1)
}
}
}
}
The code below assumes you have image assets named the same as the String Array countries. The problem is you are attempting to initialize a ForEach like you would a for...in. While they are both loops, they are not the same.
I have given example code below, but better practice would be to create a data model struct called "Country" that is Identifiable where one of the parameters is the name of the image. That way you can use one ForEach and show various data on the country, like name, population, geography, etc. var countries would then be typed like this: var countries: [Country] and your ForEach would be simplified.
struct ContentView: View {
var countries = ["dubai","Dutch","Finland","france","france","Fuji","India","Intaly","Japan","Korean","nepal","pakistan","philippe","Rusia","swiss","Tailand"].shuffled()
var body: some View {
VStack {
// The ForEach will loop through each element and assign it to
// country for use in the closure where Image() is.
ForEach(countries, id: \.self) { country in
Image(country)
.border(Color.black,width:1)
}
}
}
}

How to change values inside a picker using #Published property wrapper in swiftui

hi am having issues with the picker view in swiftui
i have created one file with just a class like this
import Foundation
import SwiftUI
class something: ObservableObject {
#Published var sel = 0
}
and then I created 2 views
import SwiftUI
struct ContentView: View {
#StateObject var hihi: something
var characters = ["Makima", "Ryuk", "Itachi", "Gojou", "Goku", "Eren", "Levi", "Jiraya", "Ichigo", "Sukuna"]
var body: some View {
VStack {
Section{
Picker("Please choose a character", selection: $hihi.sel) {
ForEach(characters, id: \.self) { name in
Text(name)
}
}
Text(characters[hihi.sel])
}
now(hihi: something())
}
}
}
struct now: View {
#StateObject var hihi: something
var body: some View {
Text("\(hihi.sel)")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(hihi: something())
}
}
now the problem am facing is that the code compiles but the picker ain't working it won't change to any other value in the array I have provided it recoils back to its original value provided that is 0th index "Makima" and it won't select any other option, why so?
please help
There are three problems, the main one being the mismatching selection.
In the Picker, your selection is based on the string value for each character. This is because the ForEach identifies each Text by the name string, since you used id: \.self.
However, your something model (which ideally should start with a capital letter by convention) has a numeric selection. Because the items in the Picker have String IDs, and this is an Int, the selection can't be set.
You can change your model to this:
class something: ObservableObject {
#Published var sel = "Makima"
}
Which also requires a slight change in the body:
VStack {
Section{
Picker("Please choose a character", selection: $hihi.sel) {
ForEach(characters, id: \.self) { name in
Text(name)
}
}
Text(hihi.sel) // Now getting string directly
}
now(hihi: something())
}
Notice we now have two views showing the selected character - but only the top one updates. The bottom one may now be redundant (the now view), but I'll show you how you can get it working anyway. This is where we encounter the 2nd problem:
You are creating a new instance of something() when passing it to now (again, should start with a capital). This means that the current instance of hihi stored in ContentView is not passed along. You are just creating a new instance of something, which uses the default value. This is completely independent from the hihi instance.
Replace:
now(hihi: something())
With:
now(hihi: hihi)
The final problem, which may not be as visible, is that you shouldn't be using #StateObject in now, since it doesn't own the object/data. Instead, the object is passed in, so you should use #ObservedObject instead. Although the example now works even without this change, you will have issues later on when trying to change the object within the now view.
Replace #StateObject in now with #ObservedObject.
Full answer (something is initialized in ContentView only for convenience of testing):
struct ContentView: View {
#StateObject var hihi: something = something()
var characters = ["Makima", "Ryuk", "Itachi", "Gojou", "Goku", "Eren", "Levi", "Jiraya", "Ichigo", "Sukuna"]
var body: some View {
VStack {
Section{
Picker("Please choose a character", selection: $hihi.sel) {
ForEach(characters, id: \.self) { name in
Text(name)
}
}
Text(hihi.sel)
}
now(hihi: hihi)
}
}
}
struct now: View {
#ObservedObject var hihi: something
var body: some View {
Text(hihi.sel)
}
}
class something: ObservableObject {
#Published var sel = "Makima"
}

SwiftUI NavigationLink

I'm working on a SwiftUI practice app and I ran into an issue with the NavigationView/NavigationLink. I am currently using the Metropolitan Museum of Art API and I wanted to make a list of departments that segues to another list of objects in that department, then that list segues to the object's information. Currently the NavigationView/NavigationLink setup I have is creating multiple NavigationViews and is resulting in multiple back buttons/navigation bars. Is there a way to have the new NavigationView replace the old one or have them work in line with one another? The only way I know how to create a segue in SwiftUI is through a NavigationView/NavigationLink but creating it twice seems to be the wrong way to go about things. I have a screen shot of the current state of my app.
App Image
This is my code at the moment.
struct ContentView: View {
#ObservedObject var model = DepartmentListViewModel()
var body: some View {
NavigationView {
List {
ForEach(model.departments, id: \.self) { department in
NavigationLink(destination: DetailView(viewModel: DetailListViewModel(selectedDepartment: department))) {
Text(department.displayName)
}
}
}
.navigationBarTitle("Departments")
}
}
}
struct DetailView: View {
#ObservedObject var viewModel: DetailListViewModel
init(viewModel: DetailListViewModel) {
self.viewModel = viewModel
}
var body: some View {
NavigationView {
List {
ForEach(viewModel.objects, id: \.self) { object in
NavigationLink(destination: ObjectView(viewModel: ObjectListViewModel(selectedObject: object))) {
Text(String(object))
}
}
}
.navigationBarTitle("ObjectIDs")
}
}
}
You don't need NavigationView in your DetailView anymore, the first one handle it