Dynamically create section for list in SwiftUI - ios
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"]
}
}
}
Related
Binding a Stepper with a Dictionary SwiftUI
I'm not sure if this is possible but I'm currently trying to create a list of steppers using a dictionary[String: Int]. Using the stepper I'm hoping to change the qty amount in the dictionary. I tried binding the value to the stepper by first doing $basic[name] and then that didn't work and so I ended up with $basic[keyPath: name] which resulted in fewer errors but still wasn't working. In the beginning I was having problems of not wanting to change the order of the dictionary that I made, and so I ended up with the ForEach below which worked for not changing the order of dictionary, however, I'm wondering if that's one of the reasons that the binding isn't working. import SwiftUI struct AllSuppliesStruct { #State var basic = ["Regular Staples": 0, "Big Staples": 0] var body: some View { Form { //Basic Supplies ForEach(basic.sorted(by: >), id: \.key) { name, qty in Stepper("\(name), \(qty)", value: $basic[keyPath: name], in: 0...10) } } } } Goal: If I pressed on the stepper only once for both Regular and Big Staples then I expect this in the dictionary basic = ["Regular Staples": 1, "Big Staples": 1]
You can manually create a Binding that acts on basic and pass that to Stepper: struct AllSuppliesStruct: View { #State var basic = ["Regular Staples": 0, "Big Staples": 0] var body: some View { Form { ForEach(basic.sorted(by: >), id: \.key) { name, qty in Stepper( "\(name), \(qty)", value: .init( get: { basic[name]! }, set: { basic[name] = $0 } ), in: 0...10 ) } } } }
How to remove a pair from a dictionary for a specified key?
I want to write a function with a parameter, which shall be used for a comparison with a key of a dictionary. The function iterates a collection and checks if the case has a pair with this key. If it has, I want to remove that pair, leave the other in that case and move on with the next case. I've created a function filterAndExtract(). However it only iterates and do nothing. When comparing (Bool) the parameter and the key in each case, it doesn't work as expected. I want to know how to identify a key in a pair, so I can do stuff with the cases in the collection. Thanks in advance! enum Tags: String { case one = "One" case two = "Two" case three = "Three" } struct Example { var title: String var pair: [Tags: String] } let cases = [ Example(title: "Random example One", pair: [Tags.one: "First preview", Tags.two: "Second preview"]), Example(title: "Random example Two", pair: [Tags.two: "Thrid preview", Tags.three: "Forth preview"]), Example(title: "Random example Three", pair: [Tags.three: "Fifth preview", Tags.one: "Sixth preview"]) ] func filterAndExtract(collection: [Example], tag: Tags) { for var item in collection { let keys = item.pair.keys for key in keys { if key == tag { item.pair.removeValue(forKey: key) } } } for i in collection { print("\(i.title) and \(i.pair.values) \nNEXT TURN--------------------------------------------------\n") } } //Results: //Random example One and ["Second preview", "First preview"] //NEXT TURN-------------------------------------------------- //Random example Two and ["Thrid preview", "Forth preview"] //NEXT TURN-------------------------------------------------- //Random example Three and ["Fifth preview", "Sixth preview"] //NEXT TURN-------------------------------------------------- //Solution (how I want it to look at the end): for var i in cases { i.pair.removeValue(forKey: .three) print("\(i.title) and \(i.pair.values) \nNEXT TURN--------------------------------------------------\n") } //Random example One and ["Second preview", "First preview"] //NEXT TURN-------------------------------------------------- //Random example Two and ["Thrid preview"] //NEXT TURN-------------------------------------------------- //Random example Three and ["Sixth preview"] //NEXT TURN--------------------------------------------------
Swift collections are value types. Whenever you assign a collection to a variable you'll get a copy of the object. To modify the parameter collections you have to make it mutable and you have to modify the value directly inside collections. func filterAndExtract(collection: [Example], tag: Tags) { var collection = collection for (index, item) in collection.enumerated() { let keys = item.pair.keys for key in keys { if key == tag { collection[index].pair.removeValue(forKey: key) } } } for i in collection { print("\(i.title) and \(i.pair.values) \nNEXT TURN--------------------------------------------------\n") } }
First of all, it's much more cleaner and reliable if you encapsulate the remove function into the Example { struct Example { var title: String var pair: [Tags: String] mutating func remove(key: Tags) -> String? { return pair.removeValue(forKey: key) } } Second of all, you can use map for tasks like this: func filterAndExtract(collection: [Example], tag: Tags) -> [Example] { return collection.map { item -> Example in var edited = item edited.remove(key: tag) print("\(edited.title) and \(edited.pair.values) \nNEXT TURN--------------------------------------------------\n") return edited } } I return some values in both functions, so you can use them if you want.
Group elements with the same property value from an array [duplicate]
This question already has answers here: How to group by the elements of an array in Swift (16 answers) Closed 6 years ago. I have an array [modelA,modelB,modelC,modelD,modelE], each element in the array is an instance of a Struct. The Struct has a property "name". for example... modelA.name = "abc" modelB.name = "efg" modelC.name = "hij" modelD.name = "abc" modelE.name = "efg" How can I group elements with the same property value into a new array? i.e. put modelA and modelD into a new array,and put modelB and modelE into another array. Assume the original array is large.
You can achieve this by using filter(_:): Returns an array containing, in order, the elements of the sequence that satisfy the given predicate. For example, consider that the structure looks like: struct Model { var name: String? } And you have an array of models: let allModelsArray = [Model(name: "abc"), Model(name: "efg"), Model(name: "hij"), Model(name: "abc"), Model(name: "efg"), Model(name: "efg"), Model(name: "hij")] So, you can get your arrays by doing (assuming that you want to filter based on the value of the name): let abcModelsArray = allModelsArray.filter { $0.name == "abc" } // [Model(name: Optional("abc")), Model(name: Optional("abc"))] let hijModelsArray = allModelsArray.filter { $0.name == "hij" } // [Model(name: Optional("hij")), Model(name: Optional("hij"))] ALSO: You mentioned that: how can I put element which has the same property value into a new array, such as put modelA and modelD into a new array, and put modelB and modelE into a new array, if array is large. Somehow, you might want to use the lazy version of the collection. Hope this helped.
I have not performance tested this struct Model { var type : String var name : String } var modelA = Model(type: "A", name: "abc") var modelB = Model(type: "B", name: "efg") var modelC = Model(type: "C", name: "abc") var modelD = Model(type: "D", name: "efg") let models = [modelA,modelB,modelC,modelD] let names = Set(models.map({return $0.name})) var groupedModels : [String:[Model]] = [:] for var name in names { let elements = models.filter({$0.name == name}) groupedModels[name] = elements }
.reduce solution: let a = [modelA, modelB, modelC, modelD, modelE] let arr = a.reduce([:]) { (result, currentModel) -> [String: [Model]] in var mutableDic = result if ((mutableDic[currentModel.name]) != nil) { mutableDic[currentModel.name]?.append(currentModel) } else { mutableDic[currentModel.name] = [currentModel] } return mutableDic } It will return the same dictionary as #Grimxn response. or got from this for loop var mutableDic = [String : [Model]]() for aModel in a { if ((mutableDic[aModel.name]) != nil) { mutableDic[aModel.name]?.append(aModel) } else { mutableDic[aModel.name] = [aModel] } } The key is to use a Dictionary to track for Model that need to be put in the same array, by comparing to it's .name.
Swift: Best way to filter an array
I have an array with student objects and another array with goodStrudentId. I need to fetch all the student objects from students array following sequence of goodStrudentId. Using multiple forloop i can able to solve this, but i want to learn the best way to solve this issue. My only problem is following sequence of goodStrudentId. Here is a sample code to understand my problem - class Student { var s_id : String var s_name : String init(i: String, n: String) { self.s_id = i; self.s_name = n; } } var students = [Student(i: "1",n: "a"), Student(i: "2",n: "b"), Student(i: "3",n: "c"), Student(i: "4",n: "d"), Student(i: "5",n: "e")] var goodStudentsId = ["5","2"] var goodStudentObject = getGoodStudentObjectUsingId(students:students, gdStudentsId:goodStudentsId) /* Expected answer: var goodStudentObject = [Student(i: "5",n: "e"), Student(i: "2",n: "b")] */ func getGoodStudentObjectUsingId(students:Array<Student>, gdStudentsId:Array<String>) -> Array<Student>! { /*?????? please complete this func*/ return []; }
func getGoodStudentObjectUsingId(students:[Student], gdStudentsId:[String]) -> [Student] { return students.filter { gdStudentsId.contains($0.s_id) } }
Find an item and change value in custom object array - Swift
I have this class class InboxInterests { var title = "" var eventID = 0 var count = "" var added = 0 init(title : String, eventID : NSInteger, count: String, added : NSInteger) { self.title = title self.eventID = eventID self.count = count self.added = added } } And i use it like this var array: [InboxInterests] = [InboxInterests]() Add item let post = InboxInterests(title: "test",eventID : 1, count: "test", added: 0) self.array.append(post) I want to find the index by eventID key and change the value of added key in the same index How is that possible?
For me, the above answer did not work. So, what I did was first find the index of the object that I want to replace then using the index replace it with the new value if let row = self.upcoming.index(where: {$0.eventID == id}) { array[row] = newValue } In Swift 5.0: if let row = self.upcoming.firstIndex(where: {$0.eventID == id}) { array[row] = newValue }
Since you are using a class, use filter and first to find the value: array.filter({$0.eventID == id}).first?.added = value In this you: filter the array down to elements that match the event ID pick the first result, if any then set the value This works since classes are pass by reference. When you edit the return value from array.filter({$0.eventID == id}).first?, you edit the underlying value. You'll need to see the answers below if you are using a struct EDIT: In Swift 3 you can save yourself a couple of characters array.first({$0.eventID == id})?.added = value EDIT: Swift 4.2: array.first(where: { $0.eventID == id })?.added = value array.filter {$0.eventID == id}.first?.added = value
The filter operator is not the best in this case, it works for some of you because classes are passed by reference. Explanation: (You can copy the following code in a playground if you want to verify it). class Book { let id: Int var title = "default" init (id: Int) { self.id = id } } var arrayBook = [Book]() arrayBook.append(Book(id: 0)) arrayBook.append(Book(id:1)) arrayBook.forEach { book in print(book.title) } arrayBook.filter{ $0.id == 1 }.first?.title = "modified" arrayBook.forEach { book in print(book.title) } Arrays are copied by value not reference, so when you are using filter you are creating a new array (different than the initial), but when you modify the new one, the initial one gets modified too because both are pointing to the same class (classed are passed by reference), so after the filter your array will have changed and the new one gets deallocated. So in this case it will print "default", "default" and then "default, "modified". What happens if you change class for struct, the value will be passed by value not reference so you will have 2 arrays in memory with different values, so if you go through arrayBooks again it will print before the filter "default","default", and then "default", "default" again. Because when you are using the filter you are creating and modifying a new array that will get deallocated if you do not store it). The solution is using map, creating a new array with all the values but with the modified items or fields that we want and then replace our array with the new one. This will print "default", "default" before the map, and then "default", "modified" This will work with structs, classes and everything that you want :). struct Book { let id: Int var title = "default" init (id: Int) { self.id = id } } var arrayBook = [Book]() arrayBook.append(Book(id: 0)) arrayBook.append(Book(id:1)) arrayBook.forEach { book in print(book.title) } arrayBook = arrayBook.map{ var mutableBook = $0 if $0.id == 1 { mutableBook.title = "modified" } return mutableBook } arrayBook.forEach { book in print(book.title) }
array = array.map { $0.eventID == id ? newValue : $0 }
If you conform your class to Equatable then this would work: extension Array where Element: Equatable { #discardableResult public mutating func replace(_ element: Element, with new: Element) -> Bool { if let f = self.firstIndex(where: { $0 == element}) { self[f] = new return true } return false } } Use like this: array.replace(prev, with: new)