I'v created a struct and I want to populate it with my data.
My struct:
struct CrimeNameSection {
var firstChar: Character
var name: [String]
var detail: [String]
var time: [String]
init(firstLetter: Character, object1: [String], object2: [String], object3: [String]) {
firstChar = firstLetter // First letter of 'name'
name = object1
detail = object2
time = object3
}
The first value of my struct ('firstChar') should hold the first letter in 'name' to create an alphabetic sections in tableView, the rest ('name','detail','time') should hold the data from my database (three columns: name, detail, time).
My code:
var marrCrimesData : NSMutableArray! // Hold the database
func getSectionsFromData() -> [CrimeNameSection] {
guard marrCrimesData != nil else {
return []
}
var sectionDictionary = [CrimeNameSection]()
for crime in marrCrimesData {
let crime = crime as! CrimesInfo
let firstChar = CrimeNameSection(firstLetter: crime.name[crime.name.startIndex], object1: [crime.name], object2: [crime.detail], object3: [crime.time])
if var names = firstChar {
names.append(crime.name)
sectionDictionary[firstChar] = names
} else {
sectionDictionary[firstChar] = [crime.name]
}
}
let sections = sectionDictionary.map { (key, value) in
return CrimeNameSection(firstLetter: key, name: value)
}
let sortedSections = sections.sorted { $0.firstLetter < $1.firstLetter }
return sortedSections
}
I get errors all over the place, I need help with storing the data inside my struct and sort it alphabetically.
Thank you all
Consider
struct Crime {
let name: String
let detail: String
let time: String
}
let crimes = [
Crime(name: "Foo", detail: "detail 1", time: "9am"),
Crime(name: "Bar", detail: "detail 2", time: "10am"),
Crime(name: "Baz", detail: "detail 3", time: "11am"),
Crime(name: "Qux", detail: "detail 4", time: "12am")
]
One approach is to just build an dictionary indexed by the first character and then sort it:
var crimeIndex = [Character: [Crime]]()
for crime in crimes {
if let firstCharacter = crime.name.characters.first {
if crimeIndex[firstCharacter] == nil {
crimeIndex[firstCharacter] = [crime]
} else {
crimeIndex[firstCharacter]?.append(crime)
}
}
}
let sortedIndex = crimeIndex.sorted { $0.0 < $1.0 }
The advantage of the above is that we can use the dictionary to efficiently find the section. If you really want to use your custom "name section" structure, I'd first make it to use an array of Crime objects (having disjointed arrays of the properties of a Crime can be fragile, e.g. if you later decide to add sorting of the crimes). So it might look like:
struct CrimeNameSection {
let firstCharacter: Character
var crimes: [Crime]
}
And because we've lost some of the Dictionary efficiency for finding the index and have manually iterate through looking for the section, and I'll go ahead and do an insertion sort at the time, saving me from having to do a separate sort later:
var crimeSections = [CrimeNameSection]()
for crime in crimes {
if let firstCharacter = crime.name.characters.first {
var hasBeenAdded = false
for (index, crimeIndex) in crimeSections.enumerated() {
if firstCharacter == crimeIndex.firstCharacter { // if we found section, add to it
crimeSections[index].crimes.append(crime)
hasBeenAdded = true
break
}
if firstCharacter < crimeIndex.firstCharacter { // if we've passed where the section should have been, insert new section
crimeSections.insert(CrimeNameSection(firstCharacter: firstCharacter, crimes: [crime]), at: index)
hasBeenAdded = true
break
}
}
// if we've gotten to the end and still haven't found section, add new section to end
if !hasBeenAdded {
crimeSections.append(CrimeNameSection(firstCharacter: firstCharacter, crimes: [crime]))
}
}
}
First of all you could not instantiate an Array and map over it like a dictionary
var sectionDictionary = [CrimeNameSection]() // Here you are init an Array
For a dictionary you have also to specify the key, for instance if the key is a string:
var sectionDictionary = [String: CrimeNameSection]() // Dictionary init
But be aware that the key have to be unique so that the dict would work properly.
Another problem here is the constructor in your .map function because you have not created a constructor for your CrimeNameSection that only takes two parameters:
init(firstLetter: Character, object1: [String], object2: [String], object3: [String]) {
firstChar = firstLetter // First letter of 'name'
name = object1
detail = object2
time = object3
}
// Another constructor with 2 arguments
init(firstLetter: Character, object1: [String]) {
firstChar = firstLetter // First letter of 'name'
name = object1
detail = []()
time = []()
}
If you don't want to use another constructor then you have to provide default values to object2 and object3 in your initial constructor.
Related
I have an array of Objects with multiple properties. I need to group it by those properties into sections. I've already wrote al algorithm which does that. However, I'd like to have a more succinct and reusable one, so that I can group items in a different manner.
Given an array of objects:
#objcMembers class Object: NSObject {
let name: UUID = UUID()
let value1: Int = Int(arc4random_uniform(6) + 1)
let value2: Int = Int(arc4random_uniform(6) + 1)
let value3: Int = Int(arc4random_uniform(6) + 1)
static func == (lhs: Object, rhs: Object) -> Bool {
lhs.name == rhs.name
}
}
[
Object1 {4, 4, 1},
Object2 {1, 3, 2},
...
Object99 {3, 4, 2},
]
... and given two data structure, Group and Section:
struct Group {
let title: String?
let sections: [Section]
}
struct Section {
let title: String?
let items: [Object]
}
I need to get the following result:
Value1: 1 // Group
Value2: 1 - Value3: 1 // Section
Object1
Object2
Object3
Value2: 1 - Value3: 2 // Section
Object1
Value2: 2 - Value3: 1 // Section
Object1
Object2
Object3
Value1: 2 // Group
Value2: 1 - Value3: 5 // Section
Object1
Value2: 4 - Value3: 1 // Section
Object1
Value2: 4 - Value3: 2 // Section
Object1
Object2
Object3
So, that the objects are grouped into sections by their Value3 and Value2 and sorted in ascending order.
Then, these sections are grouped into groups by their Value1 and, again, sorted in ascending order.
My current algorithm implemented in a basic imperative approach and I'm sure has a lot of points to be improved.
I've already tried to use Swift's Dictionary.init(grouping:by:) initialiser and then Dictionary.mapValues method to group entries further. However, Swift's dictionaries are not ordered, so I have to do a deep sort again.
Currently, my algorithm looks like this:
// Sort the array
let value1BasedDescriptors = [
NSSortDescriptor(keyPath: \Object.value1, ascending: true),
NSSortDescriptor(keyPath: \Object.value2, ascending: true),
NSSortDescriptor(keyPath: \Object.value3, ascending: true),
]
let sorted = (Array(objects) as NSArray).sortedArray(using: value1BasedDescriptors) as! [Object]
// Keep the previous object to find when one of the properties change
var previousObject: Object?
// Keep the group to be filled with sections
var currentGroup = [Section]()
// Keep the section to be filled with objects
var currentSection = [Object]()
// All the groups to be returned by the function
var groups = [Group]()
// Iterate over each object
for object in sorted {
// If it's a first time in a loop, set a previous object and skip
if previousObject == nil {
previousObject = object
// Append to the current section
currentSection.append(object)
continue
}
// If one of the value3 or value2 is different from the previously visited object -> Create a new section with the appropriate title
if object.value3 != previousObject?.value3 || object.value2 != previousObject?.value2 {
let section = Section(title: "Value2: \(previousObject?.value2) - Value3: \(previousObject?.value3)", items: currentSection)
// Add it to current group
currentGroup.append(section)
// Empty the section
currentSection.removeAll()
}
// If Value1 is different, group all the objects into group
if object.value1 != previousObject?.value1 {
let group = Group(title: "Value1: \(previousObject?.value1)", sections: currentGroup)
groups.append(group)
currentGroup.removeAll()
}
// Always add a visited object to a current section
currentSection.append(object)
// And mark as previous
previousObject = object
}
// since the last group & section won't be added in a loop, we have to add them manually
let section = Section(title: "Value2: \(previousObject?.value2) - Value3: \(previousObject?.value3)", items: currentSection)
currentGroup.append(section)
let group = Group(title: "Value1: \(previousObject?.value1)", sections: currentGroup)
groups.append(group)
debugPrint(groups)
It does exactly what I need to achieve, however, here are the limitations:
What if I want to group the objects in the following order: Value2 -> Value1 -> Value3 ? Or any other order? Then I'll have to write the same algorithm, but changing the properties
If I have to write the same algorithm multiple times, how can I make it shorter, e.g. utilising Functional or OOP methods?
Full code listing (copy-paste to Playground or the AppDelegate.swift file):
struct Group {
let title: String?
let sections: [Section]
}
struct Section {
let title: String?
let items: [Object]
}
#objcMembers class Object: NSObject {
let name: UUID = UUID()
let value1: Int = Int(arc4random_uniform(6) + 1)
let value2: Int = Int(arc4random_uniform(6) + 1)
let value3: Int = Int(arc4random_uniform(6) + 1)
static func == (lhs: Object, rhs: Object) -> Bool {
lhs.name == rhs.name
}
}
// Create a lot of objects
var objects = Set<Object>()
for i in 0...100 {
objects.insert(Object())
}
// Sort the array
let value1BasedDescriptors = [
NSSortDescriptor(keyPath: \Object.value1, ascending: true),
NSSortDescriptor(keyPath: \Object.value2, ascending: true),
NSSortDescriptor(keyPath: \Object.value3, ascending: true),
]
let sorted = (Array(objects) as NSArray).sortedArray(using: value1BasedDescriptors) as! [Object]
// Keep the previous object to find when one of the properties change
var previousObject: Object?
// Keep the group to be filled with sections
var currentGroup = [Section]()
// Keep the section to be filled with objects
var currentSection = [Object]()
// All the groups to be returned by the function
var groups = [Group]()
// Iterate over each object
for object in sorted {
// If it's a first time in a loop, set a previous object and skip
if previousObject == nil {
previousObject = object
// Append to the current section
currentSection.append(object)
continue
}
// If one of the value3 or value2 is different from the previously visited object -> Create a new section with the appropriate title
if object.value3 != previousObject?.value3 || object.value2 != previousObject?.value2 {
let section = Section(title: "Value2: \(previousObject?.value2) - Value3: \(previousObject?.value3)", items: currentSection)
// Add it to current group
currentGroup.append(section)
// Empty the section
currentSection.removeAll()
}
// If Value1 is different, group all the objects into group
if object.value1 != previousObject?.value1 {
let group = Group(title: "Value1: \(previousObject?.value1)", sections: currentGroup)
groups.append(group)
currentGroup.removeAll()
}
// Always add a visited object to a current section
currentSection.append(object)
// And mark as previous
previousObject = object
}
// since the last group & section won't be added in a loop, we have to add them manually
let section = Section(title: "Value2: \(previousObject?.value2) - Value3: \(previousObject?.value3)", items: currentSection)
currentGroup.append(section)
let group = Group(title: "Value1: \(previousObject?.value1)", sections: currentGroup)
groups.append(group)
debugPrint(groups)
Here's how I would do this. I would use Dictionary(_:groupingBy:) to produce a groups, and then take that dictionary as the input to a mapping process, transforming the key:value pairs into Group objects. The mapping itself involves another process, calling Dictionary(_:groupingBy:) to group by value2, mapping those key:value pairs into Section objects.
To add the customization you're looking for, you can replace this nesting of these repeating Dictionary(_:groupingBy:), map and sorted calls can be replaces with recursion, by taking an array of keypaths (which represent the values by which you want the various layers grouped by)
import Foundation
struct Object: Equatable {
// let name: UUID = UUID()
let value1 = Int.random(in: 1...6)
let value2 = Int.random(in: 1...6)
let value3 = Int.random(in: 1...6)
static func == (lhs: Object, rhs: Object) -> Bool {
return (lhs.value1, lhs.value2, lhs.value3) == (rhs.value1, rhs.value2, rhs.value3)
}
}
extension Object: Comparable {
static func < (lhs: Object, rhs: Object) -> Bool {
return (lhs.value1, lhs.value2, lhs.value3) < (rhs.value1, rhs.value2, rhs.value3)
}
}
struct Group: CustomDebugStringConvertible {
let title: String
let sections: [Section]
var debugDescription: String {
let sectionText = self.sections
.map { "\t" + $0.debugDescription }
.joined(separator: "\n")
return "Group: \(self.title)\n\(sectionText)"
}
}
struct Section: CustomDebugStringConvertible {
let title: String
let items: [Object]
var debugDescription: String {
let itemText = self.items
.map { "\t\t" + String(describing: $0) }
.joined(separator: "\n")
return "Section: \(self.title)\n\(itemText)"
}
}
let input = (0...100).map { _ in Object() }.sorted()
let groups = Dictionary(grouping: input, by: { $0.value1 })
.map { (arg: (key: Int, rawSections: [Object])) -> Group in
let (key, rawSections) = arg
let sections = Dictionary(grouping: rawSections, by: { $0.value2 })
.map { key, objects in
Section(title: String(key), items: objects.sorted { $0.value3 < $1.value3 })
}
.sorted { $0.title < $1.title }
return Group(title: String(key), sections: sections)
}
.sorted(by: { $0.title < $1.title })
for group in groups {
debugPrint(group)
}
I have an array of dictionaries, [[String:AnyObject]], which is reduce+sorted as below successfully.
var arrUserList = [(key: String, value: [[String : Any]])]()
let result = self.arrJsonDict.reduce(into: [String: [[String:Any]]]()) { result, element in
let strName: String = (element as! NSDictionary).value(forKey: "name") as! String
if let firstLetter = strName.first {
let initial = String(describing: firstLetter).uppercased()
result[initial, default: [[String:Any]]() ].append(element as! [String : Any])
}}.sorted { return $0.key < $1.key }
self.arrUserList = result
Now I wanted to assign keys to table sections and values as table cell text from the array.
This is very cumbersome code.
You are highly encouraged to use a struct rather than a dictionary at least with a member name
struct Person {
let name : String
}
Declare and rename arrJsonDic (more descriptively) as
var people : [Person]()
and arrUserList as
var users = [String: [Person]]()
For the sections declare another array
var letters = [String]()
Group the array and populate letters simply with
users = Dictionary(grouping: people, by: { String($0.name.first!) })
letters = users.keys.sorted()
In the table view in numberOfSections return
return letters.count
and in numberOfRows return
let letter = letters[section]
return users[letter]!.count
In cellForRowAt assign a name to a label with
let letter = letters[indexPath.section]
let user = users[letter]![indexPath.row]
cell.nameLabel.text = user.name
------------------------------
To make it still swiftier declare a second struct Section
struct Section {
let index : String
let people : [Person]
}
delete
var letters = [String]()
and declare users
var users = [Section]()
The grouping is slightly different
let grouped = Dictionary(grouping: people, by: { String($0.name.first!) })
users = grouped.map({ Section(index: $0.0, people: $0.1) }).sorted{$0.index < $1.index}
The code in the three table view delegate methods are
return users.count
-
return users[section].people.count
-
let user = users[indexPath.section].people[indexPath.row]
cell.nameLabel.text = user.name
I have this code in my viewController
var myArray :Array<Data> = Array<Data>()
for i in 0..<mov.count {
myArray.append(Data(...))
}
class Data {
var value :CGFloat
var name :String=""
init({...})
}
My input of Data is as:
10.5 apple
20.0 lemon
15.2 apple
45
Once I loop through, I would like return a new array as:
sum(value) group by name
delete last row because no have name
ordered by value
Expected result based on input:
25.7 apple
20.0 lemon
and nothing else
I wrote many rows of code and it is too confused to post it. I'd find easier way, anyone has a idea about this?
First of all Data is reserved in Swift 3, the example uses a struct named Item.
struct Item {
let value : Float
let name : String
}
Create the data array with your given values
let dataArray = [Item(value:10.5, name:"apple"),
Item(value:20.0, name:"lemon"),
Item(value:15.2, name:"apple"),
Item(value:45, name:"")]
and an array for the result:
var resultArray = [Item]()
Now filter all names which are not empty and make a Set - each name occurs one once in the set:
let allKeys = Set<String>(dataArray.filter({!$0.name.isEmpty}).map{$0.name})
Iterate thru the keys, filter all items in dataArray with the same name, sum up the values and create a new Item with the total value:
for key in allKeys {
let sum = dataArray.filter({$0.name == key}).map({$0.value}).reduce(0, +)
resultArray.append(Item(value:sum, name:key))
}
Finally sort the result array by value desscending:
resultArray.sorted(by: {$0.value < $1.value})
---
Edit:
Introduced in Swift 4 there is a more efficient API to group arrays by a predicate, Dictionary(grouping:by:
var grouped = Dictionary(grouping: dataArray, by:{$0.name})
grouped.removeValue(forKey: "") // remove the items with the empty name
resultArray = grouped.keys.map { (key) -> Item in
let value = grouped[key]!
return Item(value: value.map{$0.value}.reduce(0.0, +), name: key)
}.sorted{$0.value < $1.value}
print(resultArray)
First of all, you should not name your class Data, since that's the name of a Foundation class. I've used a struct called MyData instead:
struct MyData {
let value: CGFloat
let name: String
}
let myArray: [MyData] = [MyData(value: 10.5, name: "apple"),
MyData(value: 20.0, name: "lemon"),
MyData(value: 15.2, name: "apple"),
MyData(value: 45, name: "")]
You can use a dictionary to add up the values associated with each name:
var myDictionary = [String: CGFloat]()
for dataItem in myArray {
if dataItem.name.isEmpty {
// ignore entries with empty names
continue
} else if let currentValue = myDictionary[dataItem.name] {
// we have seen this name before, add to its value
myDictionary[dataItem.name] = currentValue + dataItem.value
} else {
// we haven't seen this name, add it to the dictionary
myDictionary[dataItem.name] = dataItem.value
}
}
Then you can convert the dictionary back into an array of MyData objects, sort them and print them:
// turn the dictionary back into an array
var resultArray = myDictionary.map { MyData(value: $1, name: $0) }
// sort the array by value
resultArray.sort { $0.value < $1.value }
// print the sorted array
for dataItem in resultArray {
print("\(dataItem.value) \(dataItem.name)")
}
First change your data class, make string an optional and it becomes a bit easier to handle. So now if there is no name, it's nil. You can keep it as "" if you need to though with some slight changes below.:
class Thing {
let name: String?
let value: Double
init(name: String?, value: Double){
self.name = name
self.value = value
}
static func + (lhs: Thing, rhs: Thing) -> Thing? {
if rhs.name != lhs.name {
return nil
} else {
return Thing(name: lhs.name, value: lhs.value + rhs.value)
}
}
}
I gave it an operator so they can be added easily. It returns an optional so be careful when using it.
Then lets make a handy extension for arrays full of Things:
extension Array where Element: Thing {
func grouped() -> [Thing] {
var things = [String: Thing]()
for i in self {
if let name = i.name {
things[name] = (things[name] ?? Thing(name: name, value: 0)) + i
}
}
return things.map{$0.1}.sorted{$0.value > $1.value}
}
}
Give it a quick test:
let t1 = Thing(name: "a", value: 1)
let t2 = Thing(name: "b", value: 2)
let t3 = Thing(name: "a", value: 1)
let t4 = Thing(name: "c", value: 3)
let t5 = Thing(name: "b", value: 2)
let t6 = Thing(name: nil, value: 10)
let bb = [t1,t2,t3,t4,t5,t6]
let c = bb.grouped()
// ("b",4), ("c",3) , ("a",2)
Edit: added an example with nil for name, which is filtered out by the if let in the grouped() function
I have a class such as this
class FoundItem : NSObject {
var id : String!
var itemName : String!
var itemId : Int!
var foundBy : String!
var timeFound : String!
init(id: String,
itemName: String,
itemId: Int,
foundBy: String,
timeFound: String)
{
self.id = id
self.itemName = itemName
self.itemId = itemId
self.foundBy = foundBy
self.timeFound = timeFound
}
and I reference it on my
class MapViewVC: UIViewController, MKMapViewDelegate {
var found = [FoundItem]()
var filterItemName : String()
}
My FoundItem are generated by into an array of dictionaries from my class of FoundItem from a firebase query. I then get a string of that itemName that is generated from an another view controller that is a collection view on the didSelection function. I want to take that string and then filter or search the arrays with the string itemName that is equal from the itemName string from my previous viewController. Then removed the array of dictionaries that are not equal to the itemName. Not just the objects, but the entire array that contains non-equal key, value pair. I have looked for days, and I am stuck on filtering an array of dictionaries created from a class. I have looked and tried NSPredicates, for-in loops, but all that ends up happening is creating a new array or bool that finds my values or keys are equal. Here is the current function I have written.
func filterArrayBySearch() {
if self.filterItemName != nil {
dump(found)
let namePredicate = NSPredicate(format: "itemName like %#", "\(filterItemName)")
let nameFilter = found.filter { namePredicate.evaluate(with: $0) }
var crossRefNames = [String: [FoundItem]]()
for nameItemArr in found {
let listName = nameItem.itemName
let key = listName
if crossRefNames.index(forKey: key!) != nil {
crossRefNames[key!]?.append(nameItemArr)
if !("\(key)" == "\(filterItemName!)") {
print("------------- Success have found [[[[[[ \(key!) ]]]]]] and \(filterItemName!) to be equal!!")
// crossRefNames[key!]?.append(nameItemArr)
} else {
print("!! Could not find if \(key!) and \(filterItemName!) are equal !!")
}
} else {
crossRefNames[key!] = [nameItemArr]
}
}
} else {
print("No Data from Search/FilterVC Controller")
}
}
Can anyone help? It seems like it would be the simple task to find the value and then filter out the dictionaries that are not equal to the itemName string, but I keep hitting a wall. And running into for-in loops myself :P trying different things to achieve the same task.
I hope I understood what you were asking. You mention an "array of dictionaries" but you don't actually have an array of dictionaries anywhere in the code you've posted.
As far as I can tell, you are asking how to find all the entries in the found array for which itemName equals the filterItemName property.
If so, all you should need to do is:
let foundItems = found.filter { $0.itemName == filterItemName }
That's it.
Some other ideas:
If you want to search for items where filterItemName is contained in the itemName, you could do something like this:
let foundItems = found.filter { $0.itemName.contains(filterItemName) }
You could also make use of the lowercased() function if you want to do case-insensitive search.
You could also return properties of your found elements into an array:
let foundIds = found.filter { $0.itemName == filterItemName }.map { $0.itemId }
Sort array of dictionary using the following way
var dict:[[String:AnyObject]] = sortedArray.filter{($0["parentId"] as! String) == "compareId"}
The filter function loops over every item in a collection, and returns a collection containing only items that satisfy an include condition.
We can get single object from this array of dictionary , you can use the following code
var dict = sortedArray.filter{($0["parentId"] as! String) == "compareId"}.first
OR
let dict = sortedArray.filter{ ($0["parentId"] as! String) == "compareId" }.first
Local search filter using predicate in array of dictionary objects
with key name this code use for both swift3 and swift4,4.1 also.
func updateSearchResults(for searchController:
UISearchController) {
if (searchController.searchBar.text?.characters.count)! > 0 {
guard let searchText = searchController.searchBar.text,
searchText != "" else {
return
}
usersDataFromResponse.removeAll()
let searchPredicate = NSPredicate(format: "userName
CONTAINS[C] %#", searchText)
usersDataFromResponse = (filteredArray as
NSArray).filtered(using: searchPredicate)
print ("array = \(usersDataFromResponse)")
self.listTableView.reloadData()
}
}
Here I use CoreData And I have Array of Dictionary .
Here I am filter the key paymentToInvoice that value is invoice array then key invoiceToPeople that key contain People Dictionary then I search FirstName, lastName, Organization multiple key contain searchText.
I hope it's helps Please try this Thank You
var searchDict = dict.filter { (arg0) -> Bool in
let (key, value) = arg0
for paymentInfo in (value as! [PaymentInfo]){
let organization = (Array((value as! [PaymentInfo])[0].paymentToInvoice!)[0] as! InvoiceInfo).invoiceToPeople?.organization
let firstName = (Array((value as! [PaymentInfo])[0].paymentToInvoice!)[0] as! InvoiceInfo).invoiceToPeople?.firstName
let lastName = (Array((value as! [PaymentInfo])[0].paymentToInvoice!)[0] as! InvoiceInfo).invoiceToPeople?.lastName
return organization?.localizedStandardRange(of: searchText) != nil || firstName?.localizedStandardRange(of: searchText) != nil || lastName?.localizedStandardRange(of: searchText) != nil
}
return true
}
I want to sort an array of performers so that they are grouped by the first character of their first name. So for example 'A' in the following output, is for a collection of performers who's first name starts with 'A'.
[
"A"[Performer,Performer,Performer,Performer]
"B"[Performer,Performer,Performer]
"C"[Performer,Performer,Performer]
"D"[Performer,Performer,Performer]
"F"[Performer,Performer,Performer]
"M"[Performer,Performer,Performer]
... etc
]
I have achieved it but I'm hoping there is a more trivial way to do it. The following is how I achieved it.
class Performer {
let firstName: String
let lastName: String
let dateOfBirth: NSDate
init(firstName: String, lastName: String, dateOfBirth: NSDate) {
self.firstName = firstName
self.lastName = lastName
self.dateOfBirth = dateOfBirth
}
}
private var keyedPerformers: [[String: [Performer]]]?
init() {
super.init()
let performers = generatePerformers()
let sortedPerformers = performers.sort { $0.0.firstName < $0.1.firstName }
keyedPerformers = generateKeyedPerformers(sortedPerformers)
}
//'sortedPerformers' param must be sorted alphabetically by first name
private func generateKeyedPerformers(sortedPerformers: [Performer]) -> [[String: [Performer]]] {
var collectionKeyedPerformers = [[String: [Performer]]]()
var keyedPerformers = [String: [Performer]]()
var lastLetter: String?
var collection = [Performer]()
for performer in sortedPerformers {
let letter = String(performer.firstName.characters.first!)
if lastLetter == nil {
lastLetter = letter
}
if letter == lastLetter {
collection.append(performer)
} else {
keyedPerformers[lastLetter!] = collection
collectionKeyedPerformers.append(keyedPerformers)
keyedPerformers = [String: [Performer]]()
collection = [Performer]()
}
lastLetter = letter
}
return collectionKeyedPerformers.sort { $0.0.keys.first! < $0.1.keys.first! }
}
First of all, since you have an (sorted) array of section index titles it's not necessary to use an array of dictionaries. The most efficient way is to retrieve the array of Performers from the dictionary by key.
Add a lazy instantiated variable initialLetter in the Performer class. I added also the description property to get a more descriptive string representation.
class Performer : CustomStringConvertible{
let firstName: String
let lastName: String
let dateOfBirth: NSDate
init(firstName: String, lastName: String, dateOfBirth: NSDate) {
self.firstName = firstName
self.lastName = lastName
self.dateOfBirth = dateOfBirth
}
lazy var initialLetter : String = {
return self.firstName.substringToIndex(self.firstName.startIndex.successor())
}()
var description : String {
return "\(firstName) \(lastName)"
}
To create the performers use an array rather than a simple string. The "missing" letters are already omitted.
private let createIndexTitles = ["A","B","C","D","E","F","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"]
The function to create the dummy instances pre-sorts the arrays of performers by initialLetter and then by lastName
private func generatePerformers() -> [Performer] {
var performers = [Performer]()
for i in 0...99 {
let x = i % createIndexTitles.count
let letter = createIndexTitles[x]
performers.append(Performer(firstName: "\(letter)", lastName: "Doe", dateOfBirth: NSDate()))
}
return performers.sort({ (p1, p2) -> Bool in
if p1.initialLetter < p2.initialLetter { return true }
return p1.lastName < p2.lastName
})
}
Now create an empty array of strings for the section title indexes
private var sectionIndexTitles = [String]()
The function to create the keyed performers follows a simple algorithm : If the key for initialLetter doesn't exist, create it. Then append the performer to the array. At the end assign the sorted keys of the dictionary to sectionIndexTitles, the elements of the keyed arrays are already sorted.
private func generateKeyedPerformers(sortedPerformers: [Performer]) -> [String: [Performer]] {
var keyedPerformers = [String: [Performer]]()
for performer in sortedPerformers {
let letter = performer.initialLetter
var keyedPerformer = keyedPerformers[letter]
if keyedPerformer == nil {
keyedPerformer = [Performer]()
}
keyedPerformer!.append(performer)
keyedPerformers[letter] = keyedPerformer!
}
sectionIndexTitles = Array(keyedPerformers.keys).sort { $0 < $1 }
return keyedPerformers
}
Now test it
let performers = generatePerformers()
let keyedPerformers = generateKeyedPerformers(performers)
print(sectionIndexTitles , keyedPerformers)
In the (presumably) table view use sectionIndexTitles as the section array and get the performer arrays by key respectively.