I am trying to dynamically create sections for List with a header in SwiftUI.
here is my array:
var lists = [a list of names with A to Z] // array of strings
then I try to get first letter:
var firstCharacters: [Character] {
var firstCharacters = [Character]()
for list in lists.sorted(by: {$0 < $1}) {
if let character = list.first, !firstCharacters.contains(character) {
firstCharacters.append(character)
}
}
return firstCharacters
}
I created the list like this:
List {
ForEach(firstCharacters, id: \.self) { charachter in
Section(header: Text("\(charachter.description)")) {
ForEach(Array(lists.enumerated()), id:\.element) { index, element in
Text("Name \(element), Id: \(index)")
})
}
}
}
}
Now I have a problem adding section to the list now sure how can I combine with the list.
A better way to section this is to take your list of words and turn it into a dictionary keyed by the first letter like this:
var wordDict: [String:[String]] {
let letters = Set(lists.compactMap( { $0.first } ))
var dict: [String:[String]] = [:]
for letter in letters {
dict[String(letter)] = lists.filter( { $0.first == letter } ).sorted()
}
return dict
}
Then use the Dict in your List like this:
List {
// You then make an array of keys, sorted by lowest to highest
ForEach(Array(wordDict.keys).sorted(by: <), id: \.self) { character in
Section(header: Text("\(character)")) {
// Because a dictionary lookup can return nil, we need to provide a response
// if it fails. I used [""], though I could have just force unwrapped.
ForEach(wordDict[character] ?? [""], id: \.self) { word in
Text("Name \(word)")
}
}
}
}
This prevents you from having to iterate through the whole list for every letter. It is done once when creating the Dict. You no longer need var firstChar.
The following would print the names in each section:
struct ContactsView: View {
let names = ["James", "Steve", "Anna", "Baxter", "Greg", "Zendy", "Astro", "Jenny"]
var firstChar: [Character] {
let array = names
.compactMap({ $0.first })
// Set is just a quick way to remove duplicates.
return Array(Set(array))
.sorted(by: { $0 < $1 })
}
var body: some View {
List {
ForEach(firstChar, id: \.self) { char in
Section(header: Text("\(char.description)")) {
ForEach(names, id: \.self) { name in
if name.first == char {
Text(name)
}
}
}
}
}
}
}
Although a better way might be to not have 2 different arrays but merge them into one where the first letter is the key and the values are the names.
That way you don't have to iterate over every name for every section.
My recommendation is a view model which groups an array of strings into a Section struct with Dictionary(grouping:by:)
class ViewModel : ObservableObject {
struct Section: Identifiable {
let letter : String
let names : [String]
var id : String { letter }
}
#Published var sections = [Section]()
var names = [String]() {
didSet {
let grouped = Dictionary(grouping: names, by: {$0.prefix(1)})
sections = grouped.keys.sorted().map{Section(letter: String($0), names: grouped[$0]!)}
}
}
}
Whenever the content of the array is being modified the section array (and the view) is updated.
In the view in onAppear pass some names
struct ContentView: View {
#StateObject private var model = ViewModel()
var body: some View {
List(model.sections) { section in
Section(section.letter) {
ForEach(section.names, id: \.self, content: Text.init)
}
}
.onAppear {
model.names = ["Aaran", "Aaren", "Aarez", "Badsha", "Bailee", "Bailey", "Bailie", "Bailley", "Carlos", "Carrich", "Carrick", "Carson", "Carter", "Carwyn", "Dante", "Danyal", "Danyil", "Danys", "Daood", "Dara", "Darach", "Daragh", "Darcy", "D'arcy", "Dareh", "Eisa", "Eli", "Elias", "Elijah", "Eliot", "Elisau", "Finn", "Finnan", "Finnean", "Finnen", "Finnlay", "Geoff", "Geoffrey", "Geomer", "Geordan", "Hamad", "Hamid", "Hamish", "Hamza", "Hamzah", "Han", "Idris", "Iestyn", "Ieuan", "Igor", "Ihtisham", "Jarno", "Jarred", "Jarvi", "Jasey-Jay", "Jasim", "Jaskaran", "Jason", "Jasper", "Jaxon", "Kabeer", "Kabir", "Kacey", "Kacper", "Kade", "Kaden", "Kadin", "Kadyn", "Kaeden", "Kael", "Kaelan", "Kaelin", "Kaelum", "Kai", "Kaid", "Kaidan", "Kaiden", "Kaidinn", "Kaidyn", "Kaileb", "Kailin", "Karsyn", "Karthikeya", "Kasey", "Kash", "Kashif", "Kasim", "Kasper", "Kasra", "Kavin", "Kayam", "Leiten", "Leithen", "Leland", "Lenin", "Lennan", "Lennen", "Lennex", "Lennon", "Lennox", "Lenny", "Leno", "Lenon", "Lenyn", "Leo", "Leon", "Leonard", "Leonardas", "Leonardo", "Lepeng", "Leroy", "Leven", "Levi", "Levon", "Machlan", "Maciej", "Mack", "Mackenzie", "Mackenzy", "Mackie", "Macsen", "Macy", "Madaki", "Nickson", "Nicky", "Nico", "Nicodemus", "Nicol", "Nicolae", "Nicolas", "Nidhish", "Nihaal", "Nihal", "Nikash", "Olaoluwapolorimi", "Ole", "Olie", "Oliver", "Olivier", "Peter", "Phani", "Philip", "Philippos", "Phinehas", "Phoenix", "Phoevos", "Pierce", "Pierre-Antoine", "Pieter", "Pietro", "Piotr", "Porter", "Prabhjoit", "Prabodhan", "Praise", "Pranav", "Rasul", "Raul", "Raunaq", "Ravin", "Ray", "Rayaan", "Rayan", "Rayane", "Rayden", "Rayhan", "Santiago", "Santino", "Satveer", "Saul", "Saunders", "Savin", "Sayad", "Sayeed", "Sayf", "Scot", "Scott", "Scott-Alexander", "Seaan", "Seamas", "Seamus", "Sean", "Seane", "Sean-James", "Sean-Paul", "Sean-Ray", "Seb", "Sebastian", "Sebastien", "Selasi", "Seonaidh", "Sephiroth", "Sergei", "Sergio", "Seth", "Sethu", "Seumas", "Shaarvin", "Shadow", "Shae", "Shahmir", "Shai", "Shane", "Shannon", "Sharland", "Sharoz", "Shaughn", "Shaun", "Tadhg", "Taegan", "Taegen", "Tai", "Tait", "Uilleam", "Umair", "Umar", "Umer", "Umut", "Urban", "Uri", "Usman", "Uzair", "Uzayr", "Valen", "Valentin", "Valentino", "Valery", "Valo", "Vasyl", "Vedantsinh", "Veeran", "Victor", "Victory", "Vinay", "Vince", "Wen", "Wesley", "Wesley-Scott", "Wiktor", "Wilkie", "Will", "William", "William-John", "Willum", "Wilson", "Windsor", "Wojciech", "Woyenbrakemi", "Wyatt", "Wylie", "Wynn", "Xabier", "Xander", "Xavier", "Xiao", "Xida", "Xin", "Xue", "Yadgor", "Yago", "Yahya", "Yakup", "Yang", "Yanick", "Yann", "Yannick", "Yaseen", "Yasin", "Yasir", "Yassin", "Yoji", "Yong", "Yoolgeun", "Yorgos", "Youcef", "Yousif", "Youssef", "Yu", "Yuanyu", "Yuri", "Yusef", "Yusuf", "Yves", "Zaaine", "Zaak", "Zac", "Zach", "Zachariah", "Zacharias", "Ziyaan", "Zohaib", "Zohair", "Zoubaeir", "Zubair", "Zubayr", "Zuriel"]
}
}
}
I am getting an error related to accessing an item in an array using the provided Index in a ForEach loop with SwiftUI.
I have an array of information that is used to pass information to a struct to render a card. I need two of these cards per HStack, so I loop over the array and call the cards like so:
ForEach(0..<array.count){item in
Card(name: array[item].name)
Card(name: array[item+1].name)
}
But this throws the error: The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions
What I'm trying to accomplish is a bunch of Horizontal stacks, with 2 items each, in a single VStack. This way i have a list of 2 side by side cards. This seemed like a simple way to just brute force that behavior, but I keep running into the error.
I am probably just going to switch to a different style of Hstack that will wrap to next line for every 3rd added the row, but I'd still like to know if anyone can tell me why this error occurs. It appears to be such a simple operation, yet it can't be done by the compiler
Here is the actual code I'm running, if the sample above doesn't cut it. The strangest thing about this code is that it only fails after the SECOND item +1. I can run this if i only do it once in the code.
ForEach(0..<self.decks.count){item in
HStack(spacing: 30){
if(item+1 < self.decks.count){
StudyCards(cardTitle: self.decks[item].deckTitle, cardAmt: self.decks[item].stackAmount, lastStdy: self.decks[item].lastStudied)
StudyCards(cardTitle: self.decks[item+1].deckTitle, cardAmt: self.decks[item+1].stackAmount, lastStdy: self.decks[item+1].lastStudied)
}
Spacer()
.padding(.bottom, 4)
} else{
StudyCards(cardTitle: self.decks[item].deckTitle, cardAmt: self.decks[item].stackAmount, lastStdy: self.decks[item].lastStudied)
}
}
}
I replicated your error, as you said, it only happens when you have more than one "item+1" inside ForEach, like this:
Text(self.array[item + 1] + " - " + self.array[item + 1])
So I think the problem is not related to your specific Views. One solution is to create a function that increments item and returns your view:
struct ContentView: View {
#State var array: [String] = ["zero","one","two", "three"];
var body: some View {
VStack {
ForEach(0..<array.count) { item in
Text(self.array[item])
if item + 1 < self.array.count {
self.makeView(item: item)
}
}
}
}
func makeView(item: Int) -> Text {
let nextItem = item + 1
return Text(self.array[nextItem] + " - " + self.array[nextItem])
}
}
Reference: swiftui-increment-variable-in-a-foreach-loop
Alright so I got a solution!
I used this implementation of a view builder here: (https://www.hackingwithswift.com/quick-start/swiftui/how-to-position-views-in-a-grid)
struct GridStack<Content: View>: View {
let rows: Int
let columns: Int
let content: (Int, Int) -> Content
var body: some View {
VStack(spacing: 30){
ForEach(0 ..< rows, id: \.self) { row in
HStack(spacing: 30){
ForEach(0 ..< self.columns, id: \.self) { column in
self.content(row, column)
}
}
}
}
}
init(rows: Int, columns: Int, #ViewBuilder content: #escaping (Int, Int) -> Content) {
self.rows = rows
self.columns = columns
self.content = content
}
}
And then i call it like so below. I had to add the if statement because without it, it was rendering an extra card. Now i have successfully implemented a grid with 2 columns, and any amount of rows! No need for 3rd party libs!
GridStack(rows: self.rows, columns: 2){ row, col in
if(row*2 + col < self.count){
StudyCards(cardTitle: self.decks[row*2 + col].deckTitle, cardAmt: self.decks[row*2 + col].stackAmount, lastStdy: self.decks[row*2 + col].lastStudied)
}