Iterate over two arrays simultaneously in List - swiftui

How can I iterate two array in a list:
struct ContentView: View {
let colors = ["red", "green", "blue"]
let names = ["John", "Apple", "Seed"]
var body: some View {
VStack {
List(colors, id: \.self) { color in
Text(color)
}
}
}
}
For example I need to have:
Text("\(color) - \(animal)")
My code would be like this (I know it's wrong but that's the idea):
List(colors, animals id: \.self) { color, animal in
Text("\(color) - \(animal)")
}

A bit simpler
var body: some View {
VStack {
List(Array(zip(colors, names)), id: \.self.0) { (color, name) in
Text("\(color) - \(name)")
}
}
}
Update: added variant for non-equal-size arrays
This one of course is a bit complicated, but might be helpful
var body: some View {
VStack {
ListOfPairs()
}
}
private func ListOfPairs() -> some View {
var iter = names.makeIterator()
let container = colors.reduce(into: Array<(String,String)>()) { (result, color) in
result.append((color, iter.next() ?? "None" )) // << placeholder for empty
}
return List(container, id: \.self.0) { (color, name) in
Text("\(color) - \(name)")
}
}

You could make those two arrays into an object for each item, as they are related to each other. This can be made like so:
struct Object: Identifiable {
let id: UUID = UUID()
let color: String
let name: String
}
let objects = [Object(color: "red", name: "John"),
Object(color: "green", name: "Apple"),
Object(color: "blue", name: "Seed")]
And used like so:
List(objects) { object in
Text("\(object.color) - \(object.name)")
}

I think this is all a bit cumbersome and creating types that don't need to be. For this reason SwiftUI has the ForEach Statement. The code can also look like this:
import SwiftUI
struct ContentView: View {
let colors = ["red", "blue", "black", "purple", "green"]
let names = ["Paul", "Chris", "Rob", "Terry", "Andy"]
var body: some View {
List {
ForEach(0 ..< colors.count) {
Text("Name \(self.names[$0]) has favorite color \(self.colors[$0]).")
}
.onDelete(perform: deleteRow)
}
}
}
The result then looks like this:
Of course, both arrays need to have the same number of elements.

Or, if you want to get two nice columns that you can manipulate the way you want it:
import SwiftUI
struct ContentView: View {
let colors = ["red", "blue", "black", "purple", "green"]
let names = ["Paul", "Chris", "Rob", "Terry", "Andy"]
var body: some View {
HStack {
List(names, id: \.self) { name in
Text(name)
}
.frame(width: 130)
List(colors, id: \.self) { color in
Text(color)
}
.frame(width: 160)
}
}
}
And then you get the result like this:

Related

Same ForEach loop twice in one SwiftUI View

When I use a ForEach loop over an array twice within a view, I get the following warning at runtime:
LazyVGridLayout: the ID 84308994-9D16-48D2-975E-DC40C5F9EFFF is used by multiple child views, this will give undefined results!
The reason for this is clear so far, but what is the smartest way to work around this problem?
The following sample code illustrates the problem:
import SwiftUI
// MARK: - Model
class Data: ObservableObject
{
#Published var items: [Item] = [Item(), Item(), Item()]
}
struct Item: Identifiable
{
let id = UUID()
var name: String = ""
var description: String = ""
}
// MARK: - View
struct MainView: View {
#StateObject var data: Data
private var gridItems: [GridItem] { Array(repeating: GridItem(), count: data.items.count) }
var body: some View {
LazyVGrid(columns: gridItems, alignment: .leading, spacing: 2) {
ForEach(data.items) { item in
Text(item.name)
}
ForEach(data.items) { item in
Text(item.description)
}
}
}
}
// MARK: - App
#main
struct SwiftUI_TestApp: App {
var body: some Scene {
WindowGroup {
MainView(data: Data())
}
}
}
I could possibly divide the view into several SubViews.
Are there any other options?
Edit:
This is the body of the real app:
var body: some View {
VStack {
LazyVGrid(columns: columns, alignment: .leading, spacing: 2) {
Text("")
ForEach($runde.players) { $player in
PlayerHeader(player: $player)
}
ForEach(Score.Index.allCases) { index in
Text(index.localizedName)
ForEach(runde.players) { player in
Cell(player: player, active: player == runde.activePlayer, index: index)
}
}
Text ("")
ForEach(runde.players) { player in
PlaceView(player: player)
}
}
.padding()
}
}
If you really need that kind of grid filling, then it is possible just to use different identifiers for those ForEach containers, like
LazyVGrid(columns: gridItems, alignment: .leading, spacing: 2) {
ForEach(data.items) { item in
Text(item.name).id("\(item.id)-1") // << here !!
}
ForEach(data.items) { item in
Text(item.description).id("\(item.id)-2") // << here !!
}
}
Tested with Xcode 13beta / iOS 15
While adding identifiers within the ForEach loop sometimes works, I found that accessing the indexes from the loop with indices worked in other cases:
ForEach(items.indices, id: \.self) { i in
Text(items[i])
}

Picker selection not updating

I'm trying to select a default account number from a list of available accounts. The data is from an FMDB data selection. I've created a test view with two types of pickers in it. One that lists the accounts I've retrieved into an array of data records which is a swift struct. The other picker is one that comes from an on-line example to select colors. The colors "selection" bound value updates as expected, but the "selection" bound value that I set does not change when one of two accounts is presented and selected. Below is the code from my test view that compiles and runs. When I select either account value which is [12345678] or [12345679] which appear as two rows in the picker the selection binding value doesn't change. But for the colors selection value it updates. I'm pretty confused here...
The struct for the accounts record is:
// Account record for FMDB
struct AccountRecord: Hashable {
var account_id: Int!
var account_code: Int!
var account_name: String!
var running_balance: Double!
var hidden_balance: Double!
var actual_balance: Double!
}
import SwiftUI
struct PickerTestView: View {
#State private var selectedAccount = 0
#State private var selectedColor = 0
var acctRecords: [Accounts.AccountRecord] {
return Accounts.shared.selectAllAccounts()
}
var colors = ["Red", "Green", "Blue", "Tartan"]
var body: some View {
VStack{
Picker(selection: $selectedAccount, label: Text(""))
{
ForEach (self.acctRecords, id: \.self) { acct in
Text("\(acct.account_code!)")
}
}
Text("selectedAccount = \(selectedAccount)")
.font(.largeTitle)
Picker(selection: $selectedColor, label: Text("Please choose a color")) {
ForEach(0 ..< colors.count) {
Text(self.colors[$0])
}
}
Text("Selectedcolor = \(selectedColor)")
Text("You selected \(colors[selectedColor])")
}
}
}
struct PickerTestView_Previews: PreviewProvider {
static var previews: some View {
PickerTestView()
}
}
Two things are happening:
You need a .tag() on your Picker elements to tell the system which element belongs to what item:
Picker(selection: $selectedAccount, label: Text(""))
{
ForEach (self.acctRecords, id: \.self) { acct in
Text("\(acct.account_code!)").tag(acct.account_id)
}
}
SwiftUI needs the types of the selection parameter and the tag type to be the same. Because in your model, account_id is defined as Int! and not Int, your selectedAccount needs to be Int! as well:
#State private var selectedAccount : Int! = 0
The following works with some test data embedded in:
struct PickerTestView: View {
#State private var selectedAccount : Int! = 1
#State private var selectedColor = 0
var acctRecords: [AccountRecord] {
return [.init(account_id: 1, account_code: 1, account_name: "1", running_balance: 0, hidden_balance: 0, actual_balance: 0),
.init(account_id: 2, account_code: 2, account_name: "2", running_balance: 0, hidden_balance: 0, actual_balance: 0),
.init(account_id: 3, account_code: 3, account_name: "3", running_balance: 0, hidden_balance: 0, actual_balance: 0)
]
}
var colors = ["Red", "Green", "Blue", "Tartan"]
var body: some View {
VStack{
Picker(selection: $selectedAccount, label: Text(""))
{
ForEach (self.acctRecords, id: \.self) { acct in
Text("\(acct.account_code!)").tag(acct.account_id)
}
}
Text("selectedAccount = \(selectedAccount)")
.font(.largeTitle)
}
}
}
Try to add .onChange:
Picker(selection: $selectedAccount, label: Text("")) {
ForEach (self.acctRecords, id: \.self) { acct in
Text("\(acct.account_code!)").tag(acct.account_code) // <- add tag here
}
}
.onChange(of: selectedAccount) {
selectedAccount = $0
}

Picker appears disabled inside a Form... bug?

Consider this code:
struct ContentView: View {
var colors = ["Red", "Green", "Blue", "Tartan"]
#State private var selectedColor = "Red"
var body: some View {
Form {
Section (header:Text("color")) {
Picker("Please choose a color", selection: $selectedColor) {
ForEach(colors, id: \.self) {
Text($0)
}
}
}
}
}
}
Run it. The result is the picker disabled. Why? How to I enable it?
The problem is the picker being inside a form. Section makes no difference.
You have to add Form in Navigation View
struct ContentView: View {
var colors = ["Red", "Green", "Blue", "Tartan"]
#State private var selectedColor = "Red"
var body: some View {
NavigationView {
Form {
Section (header:Text("color")) {
Picker("Please choose a color", selection: $selectedColor) {
ForEach(colors, id: \.self) {
Text($0)
}
}
}
}
.navigationBarTitle("Color Picker")
}
}
}

How to create SwiftUI List of NavigationLinks with dynamic view names

I am trying to create a simple list of Views for the user to visit, I cannot figure out how to replace view name with an array variable. In the example below destination: is hard coded as AVExample(), which is one of my views, but how do I use the names in the array?
struct test: View {
var views = ["AVExample", "ColorPickerExample", "DatePickerExample"]
var body: some View {
NavigationView {
List (views, id: \.self){ view in
NavigationLink(
destination: AVExample(),
label: {
Text("\(view)")
})
}
}
}
}
You can create a struct for the views and then use that array of structure. For Example:
struct ViewsList: Identifiable {
static func == (lhs: ViewsList, rhs: ViewsList) -> Bool {
return lhs.id == rhs.id
}
var id: Int
var name: String
var viewContent: AnyView
}
And then in your view class(test), create an array of ViewList structure:
var views = [ViewsList.init(id: 1, name: "Example", viewContent: AnyView(AVExample())), ViewsList.init(id: 2, name: "ColorPicker", viewContent: AnyView(ColorPicker()))]
Then you can loop over this array as below :
NavigationView {
List (views, id: \.id){ view in
NavigationLink(
destination: view.viewContent,
label: {
Text("\(view.name)")
})
}
Many thanks for the replies. I came up with the following solution which seems to work well.
private struct aView {
var id = UUID()
var view: AnyView
var label: String
}
private var views = [
aView(view: AnyView(AppStorageExample()), label: "App Storage"),
aView(view: AnyView(AppStoreRecommend()), label: "App Store Recommended"),
aView(view: AnyView(AVExample()), label: "AV Player"),
aView(view: AnyView(ColorPickerExample()), label: "Color Picker"),
]
var body: some View {
List (views, id: \.id) { view in
NavigationLink(
destination: view.view,
label: {
Text("\(view.label)")
})
}
}

Expandable List in SwiftUI only expands once and won't contract

I'm trying (and failing!) to implement an expandable list of questions and answers in SwiftUI.
struct FAQ: Equatable, Identifiable {
static func ==(lhs: FAQ, rhs: FAQ) -> Bool {
return lhs.id == rhs.id
}
let id = UUID()
let question: String
let answers: [String]
var isExpanded: Bool = false
}
struct ContentView: View {
#State private (set) var faqs: [FAQ] = [
FAQ(question: "What is the fastest animal on land?", answers: ["The cheetah"]),
FAQ(question: "What colours are in a rainbox?", answers: ["Red", "Orange", "Yellow", "Blue", "Indigo", "Violet"])
]
var body: some View {
List {
ForEach(faqs) { faq in
Section(header: Text(faq.question)
.onTapGesture {
if let index = self.faqs.firstIndex(of: faq) {
self.faqs[index].isExpanded.toggle()
}
}
) {
if faq.isExpanded {
ForEach(faq.answers, id: \.self) {
Text("• " + $0).font(.body)
}
}
}
}
}
}
}
Tapping any question successfully expands the answers into view, but tapping the same header again doesn't contract the answers, nor does tapping a second question expand those answers.
With some judiciously placed prints, I can see that isExpanded is toggled to true on the first question tapped, but then won't toggle back to false.
Could someone explain what I'm doing wrong?
The issue is with your #State var faq: [FAQ] line. The #State property wrapper allows your view to watch for changes in your array, but changing a property of one of the array's elements does not count as a change in the array.
Instead of using a state variable, you probably want to create a small view model, like so:
class ViewModel: ObservableObject {
#Published var faqs: [FAQ] = [
FAQ(question: "What is the fastest animal on land?", answers: ["The cheetah"]),
FAQ(question: "What colours are in a rainbox?", answers: ["Red", "Orange", "Yellow", "Blue", "Indigo", "Violet"])
]
}
and update your ContentView:
struct ContentView: View {
#ObservedObject var model = ViewModel()
var body: some View {
List {
ForEach(model.faqs.indices) { index in
Section(header: Text(self.model.faqs[index].question)
.onTapGesture {
self.model.faqs[index].isExpanded.toggle()
}
) {
if self.model.faqs[index].isExpanded {
ForEach(self.model.faqs[index].answers, id: \.self) {
Text("• " + $0).font(.body)
}
}
}
}
}
}
}
The #ObservedObject property wrapper in your ContentView now watches for any changes its ViewModel object announces, and the #Published wrapper tells the ViewModel class to announce anything that happens to the array, including changes to its elements.
By splitting the views into smaller self contained views, this can be easily achieved.
The model doesn't need to know whether the answer is displayed in the screen or not. Hence, it is not required to look in the array to know whether a view is expanded.
Also, the entire list need not be redrawn when one of the sections is expanded. For more info on when a view redraws, see this article.
struct FAQ {
var question: String
var answers: [String]
}
struct InfoView: View {
let information: [FAQ] = [
FAQ(question: "What is the fastest animal on land?", answers: ["The cheetah"]),
FAQ(question: "What colours are in a rainbox?", answers: ["Red", "Orange", "Yellow", "Blue", "Indigo", "Violet"])
]
var body: some View {
List {
ForEach(information, id: \.question) { info in
InfoSection(info: info)
}
}
}
}
struct InfoSection: View {
let info: FAQ
#State var showsAnswer = false
var body: some View {
Section(header: questionHeader) {
if showsAnswer {
ForEach(info.answers, id: \.self) { answer in
Text("• " + answer)
}
}
}
}
var questionHeader: some View {
Text(info.question)
.foregroundColor(.primary)
.onTapGesture {
self.showsAnswer.toggle()
}
}
}