SwiftUI: Using a ForEach to dynamically load Images - swiftui

I have a handful of images with names such as area, volume, etc.
I want to use data to drive the images displayed. This is an example of what places can be:
let places = {
names: ["area", "volume"]
}
I tried something like this below but getting Referencing initializer 'init(_:content:)' on 'ForEach' requires that 'String' conform to 'Identifiable'
ForEach(places.name) { place in
NavigationLink (destination: Any()) {
HStack {
Image(place)
}

For types like String that don't directly conform to Identifiable, you can tell the ForEach what property to use as an id. Often with String, you'll want to use .self (note this can yield funny results if the Strings are not unique).
struct Places {
var names : [String]
}
struct ContentView : View {
let places : Places = Places(names: ["area", "volume"])
var body: some View {
ForEach(places.names, id:\.self) { place in
NavigationLink (destination: Text("Detail")) {
HStack {
Image(place)
}
}
}
}
}

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

SwiftUI Dynamically Create Enum or

I have a custom picker where the contents of the picker are created from a downloaded JSON file. In an effort to provide better accessibility for challenged users, I am trying to use the .accessibilityFocused modifier to focus VO on a user's previous selection. This means that when the user first views the picker it should start at the top, but if they go back in to change their selection, it should auto-focus on their previous selection instead of having them start over from the top of the list.
The issue is that to use .accessibilityFocused you really need to do so via an enum so you can call something like .accessibilityFocused($pickerAccessFocus, equals: .enumValue). As I am using a ForEach loop to create the picker based off of the array that is created when the JSON is parsed and then stored in the struct, I haven't figured out how to create the various enum cases based off of that array.
So, to recap:
Inside the ForEach loop, I need to have a .accessibilityFocused modifier identifying each of the picker options
The onAppear needs to say something along the lines of...
if salutation == salutation.salutation {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
pickerAccessFocus = .ENUMCASE
}
} else {
pickerAccessFocus = Optional.none
}
Though apparently onAppear does not like to have a ForEach statement, so I'm not sure how to solve that issue either.
I know there's a lot of code in the example I provided, but I tried to combine it all into as simple as example as possible. Hoping it makes sense.
If I'm thinking about it wrong, and there's a better solution, I'm all ears.
import SwiftUI
struct Picker: View {
#AccessibilityFocusState var pickerAccessFocus: PickerAccessFocus?
#State private var salutation = ""
var salutationList: [SalutationOptions] = [] // SalutationOptions is the struct from the parsed JSON
enum PickerAccessFocus: Hashable {
case ? // These cases need to be dynamically created as the same values the ForEach loop uses
}
static func nameSalutationPicker(name: String) -> LocalizedStringKey { LocalizedStringKey(String("NAME_SALUTATION_PICKER_\(name)")) }
var body: some View {
List {
Section {
ForEach(salutationList, id: \.id) { salutation in
HStack {
Text(nameSalutationPicker(name: salutation.salutation))
} // End HStack
.contentShape(Rectangle())
.accessibilityFocused(salutation == salutation.salutation ? ($pickerAccessFocus, equals: .ENUMCASE) : ($pickerAccessFocus, equals: Optional.none))
} // End ForEach
} // End Section
} // End List
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
pickerAccessFocus = .ENUMCASE
}
}
}
}
You're making it much more complicated than necessary. You don't need an enum, you can use any value, even your array values directly.
struct ContentView: View {
#AccessibilityFocusState var pickerAccessFocus: SalutationOptions?
#State private var salutation = ""
var salutationList: [SalutationOptions] = []
var body: some View {
List {
Section {
ForEach(salutationList, id: \.id) { salutation in
HStack {
Text(salutation.salutation)
} // End HStack
.contentShape(Rectangle())
.accessibilityFocused($pickerAccessFocus, equals: salutation)
} // End ForEach
} // End Section
} // End List
}
}

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