How to loop through keys in struct for SwiftUI? - swiftui

I have done XCode a long time ago and now coming back to pick up SwiftUI. I am trying to render the data of a struct in a HStack.
The struct looks like this:
struct Product: Hashable, Codable{
var column0: Int
var column1: String
var column2: String
var column3: String
var column4: String
}
Inside the view body there's something like this:
var pdt: Product
var body: some View {
HStack {
Text(pdt.column1)
Text(pdt.column2)
}
}
The above will work, but I didn't want to hardcode column1 or column2 because the number of columns will differ. How can I use pdt as a Dictionary and loop through the keys so that I can display all columns?
Many thanks in advance!

Here is a demo of possible approach (for this simple case) - you can reflect properties and map them to shown values (for custom/complex types it will require more efforts by idea should be clear).
var body: some View {
let columnValues = Mirror(reflecting: pdt)
.children.map { "\($0.value)" }
return HStack {
ForEach(columnValues, id: \.self) { Text($0) }
}
}

Related

Text view is not show in LazyVGrid

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.

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

Why #Published updates Text but doesn't update List?

I don't understand why the Text updates but the List does not.
I have a model with a #Published which is an array of dates.
There's also a method to generate text from the dates.
Here's a simplified version:
class DataModel: NSObject, ObservableObject {
#Published var selectedDates: [Date] = []
func summary() -> String {
var lines: [String] = []
for date in selectedDates.sorted().reversed() {
let l = "\(someOtherStuff) \(date.dateFormatted)"
lines.append(l)
}
return lines.joined(separator: "\n")
}
}
And I show this text in my view. Here's a simplified version:
struct DataView: View {
#ObservedObject var model: DataModel
var body: some View {
ScrollView {
Text(model.summary())
}
}
}
It works: when the user, from the UI in the View, adds a date to model.selectedDates, the summary text is properly udpated.
Now I want to replace the text with a List of lines.
I change the method:
func summary() -> [String] {
var lines: [String] = []
for date in selectedDates.sorted().reversed() {
let l = "\(someOtherStuff) \(date.dateFormatted)"
lines.append(l)
}
return lines
}
And change the view:
var body: some View {
ScrollView {
List {
ForEach(model.summary(), id: \.self) { line in
Text(line)
}
}
}
}
But this doesn't work: there's no text at all in the list, it is never updated when the user adds a date to model.selectedDates.
What am I doing wrong?
I was able to find this answer to a previous post on SO that may be the solution to your problem. When you say that there was no text at all in the list, did you debug to verify that, or was there simply no data being shown on the screen?
When I made a copy of your code snippet and fiddled with it, it appeared that there actually was data in the list that the List was attempting to display on the screen, but the frame of the List was being overridden by the Scrollview. Nesting everything inside of a GeometryReader and giving a frame size to the List gave the program the functionality it sounds like you are looking for.
Either use a ScrollView or a List for your data. If a List has multiple values, it automatically turns into a scrollable List. So, I believe you can just remove the ScrollView entirely.
Here is a fixed worked variant (with some replications - so you need to adapt it back to your project).
Tested with Xcode 12.1 / iOS 14.1
class DataModel: NSObject, ObservableObject {
#Published var selectedDates: [Date] = []
func summary() -> [String] {
var lines: [String] = []
for date in selectedDates.sorted().reversed() {
let l = "someOtherStuff \(date)"
lines.append(l)
}
return lines
}
}
struct DataView: View {
#ObservedObject var model: DateDataModel = DataModel()
var body: some View {
VStack {
Button("Add") { model.selectedDates.append(Date())}
List {
ForEach(model.summary(), id: \.self) { line in
Text(line)
}
}
}
}
}

How to count the number of rows in a list? SwiftUI

I'm currently a beginner in swiftUI and I just wanted to know how to calculate the number of rows in a list. For example: Lets say my list has x rows:
List {
Text("row1")
Text("row2")
Text("row3")
//and so on....
}
How would I find how many rows are there? I've tried researching this, but I just come across more harder and complex code.
Instead of the x Text elements in your example you could use an array with a state wrapper:
struct ExampleView: View {
#State var rowElements: [String] = ["row1", "row2", "row3"]
var body: some View {
List(rowElements, id: \.self) {rowElement in
Text(rowElement)
}
}
When you now add/remove Elements from the array your List automatically updates. That means the number of List rows equals the number of Strings contained in rowElements and can be read with rowElements.count()
This will help both of you to do it way easier without having to hard code or even create that array.
import SwiftUI
struct Example: View {
var body: some View {
List(0 ..< 5) { item in
DetailExampleView(count: item)
}
}
}
struct Example_Previews: PreviewProvider {
static var previews: some View {
Example()
}
}
struct DetailExampleView: View {
#State var count: Int = 0
var body: some View {
Text("Item no.\(count)")
}
}
Anyway, if you would like your count to start on 1 instead of 0, just add
DetailExampleView(count: item + 1)