Filtering an Array inside a Dictionary - Swift - swift

I am trying to search through a indexed dictionary to return a specific client based on the client's last name. Below are the data structures I am using. Each client object has a name property which is a String.
var clients = Client.loadAllClients() //Returns client array
var contacts = [String: [Client]]() //Indexed clients in a dictionary
var letters: [String] = []
var filteredClient = [Client]()
var shouldShowSearchResults = false
var searchController : UISearchController!
When I do my indexing, the contacts dictionary returns:
{A: [Client("Andrew")]}
Letters array returns:
[A]
I am using the UISearchController to display the filtered array of clients.
func updateSearchResults(for searchController: UISearchController) {
// how to filter the dictionary
self.tableView.reloadData()
}
However, I have no idea how to filter the dictionary to return the correct list of clients. I have tried to use
contacts.filter(isIncluded: ((key: String, value: [Client])) throws -> Bool((key: String, value: [Client])) throws -> Bool)
But I was very confused about the implementation. I am using Xcode 8.0 and Swift 3.0.
If anyone could point me in the right direction, that would be much appreciated. Please let me know if I need to clarify anything. Thank you in advance. The full code can be found at my Github

The main problem is that you are using a dictionary as data source array.
My suggestion is to use a custom struct as model
struct Contact {
let letter : String
var clients : [Client]
init(letter: String, clients : [Client] = [Client]()) {
self.letter = letter
self.clients = clients
}
mutating func add(client : Client) {
clients.append(client)
}
}
Then create your data source array
var contacts = [Contact]()
and the letter array as computed property
var letters : [String] = {
return contacts.map{ $0.letter }
}
It's easy to sort the array by letter
contacts.sort{ $0.letter < $1.letter }
Now you can search / filter this way (text is the text to be searched for)
filteredClient.removeAll()
for contact in contacts {
let filteredContent = contact.clients.filter {$0.name.range(of: text, options: [.anchored, .caseInsensitive, .diacriticInsensitive]) != nil }
if !filteredContent.isEmpty {
filteredClient.append(filteredContent)
}
}
You can even keep the sections (letters) if you declare filteredClient also as [Contact] and create temporary Contact instances with the filtered items.
Of course you need to change all table view data source / delegate methods to conform to the Contact array, but it's worth it. An array as data source is more efficient than a dictionary.

Related

Filter An Array Of Arrays By A Value In Firestore Swift

I'm looking to filter an array of arrays by specific value of one of the keys located within each array. Each nested array is read in from Firestore.
As an object, each nested array would look like this:
struct Video {
var url: String
var submissionDate: Timestamp
var submittingUser: String
}
I'm reading it in like this:
videos = document.get("videos") as? [[String : Any]] ?? nil
So far so good, but when I filter it like this:
filteredVideos = videos.filter { $0[2].contains(self.userIdentification) }
I can't do it without getting the error "Reference to member 'contains' cannot be resolved without a contextual type," an error which I was unable to find any relevant information on SO about.
I have read that some people say "Don't use arrays in Firestore!" but this is a build requirement.
Anyone have any ideas? Basically just need all arrays within the array where userId == submittingUser.
Reference Article:
I tried the answer from here: How to filter out a Array of Array's but no luck for this situation.
It's actually an array of dictionaries, not an array of arrays. All you need to do is construct the right predicate for the filter.
This is basically what your Firestore data looks like:
let videos: [[String: Any]] = [
["url": "http://www...", "submittingUser": "user1"],
["url": "http://www...", "submittingUser": "user2"]
]
This is the user you're looking for:
let userIdentification = "user2"
This is the predicate for the filer:
let filteredVideos = videos.filter { (video) -> Bool in
if let submittingUser = video["submittingUser"] as? String,
submittingUser == userIdentification {
return true
} else {
return false
}
}
You can shorthand this down to a single line if you're okay with force-unwrapping the dictionary (if you're 100% certain every video will have a valid submittingUser value):
let filteredVideos = videos.filter({ $0["submittingUser"] as! String == userIdentification })

Problem with mapping an array of objects in Swift

I have the following 2 class / structs:
class ConversationDetails {
var messages: [ChatMessage]?
var participants: [User]?
}
class User: Codable {
init (email: String) {
self.email = email
}
// system
var id: String?
var verifiedaccount: Int?
var rejected: Int?
...
}
I've further got the var conversationDetails = ConversationDetails () and I'm populating it with an API call. That all works fine.
I'd like to map the participants array inconversationDetailsand access the id property of each participant like so:
let recipient_ids = self.conversationDetails.participants.map( { (participant) -> String in
return participant.id
})
In my understanding, map iterates over the entire participants array, which is an array of User objects and I can access each item via participant.
However, I get Value of type '[User]' has no member 'id' for return participant.id.
Where is my misunderstanding?
Your participants var is optional, so you need to add question mark to access array. Without it, you try to call map on optional.
This is working code:
let recipientIds = conversationDetails.participants?.map( { (participant) -> String in
return participant.id
})
or shorter:
let recipientIds = conversationDetails.participants?.map { $0.id }
Also, you can use compactMap to remove nils from recipientIds array and have [String] array instead of [String?]:
let recipientIds = conversationDetails.participants?.compactMap { $0.id }
The first problem is that participants is optional so you need to add a ? when accessing it
conversationDetails.participants?
then when mapping you should use compactMap since id is also an optional property
let recipient_ids = conversationDetails.participants?.compactMap { $0.id }
Another variant is to not have an optional array but instead initialize it to an empty array. This is actually a much better way to handle collection properties because you can have a cleaner code by initializing them to an empty collection
var participants = [User]()
and then do
let recipient_ids = conversationDetails.participants.compactMap { $0.id }

Trouble using UILocalizedIndexedCollation to filter a table view with the inputed user's name.

I'm new to this forum! I am having a little bit of trouble sorting my Person's array with the use of UILocalizedIndexedCollation.
Code
I created the following class:
#objc class Person : NSObject {
#objc var firstName: String!
#objc var lastName: String!
init(firstName: String, lastName: String) {
self.firstName = firstName
self.lastName = lastName
}
}
With the help of this class, I created a bunch of instances to be displayed and sorted alphabetically on the table view by their last names.
For me to do so, I created the following function:
func partitionObjects(array:[AnyObject], collationStringSelector:Selector) -> ([AnyObject], [String]) {
var unsortedSections = [[AnyObject]]()
//1. Create a array to hold the data for each section
for _ in self.sectionTitles {
unsortedSections.append([]) //appending an empty array
}
//2. Put each objects into a section
for item in array {
let index:Int = self.section(for: item, collationStringSelector:collationStringSelector)
unsortedSections[index].append(item)
}
//3. sorting the array of each sections
var sectionTitles = [String]()
var sections = [AnyObject]()
sectionTitles.append("Frequently Requested")
for index in 0 ..< unsortedSections.count { if unsortedSections[index].count > 0 {
sectionTitles.append(self.sectionTitles[index])
sections.append(self.sortedArray(from: unsortedSections[index], collationStringSelector: collationStringSelector) as AnyObject)
}
}
return (sections, sectionTitles)
}
With this function, I am able to assign my array of Person and array of titles to the returning values of the function:
var sectionTitles = [Person]()
var personsWithSections = [[Person]]()
var contacts = [Person]()
#objc func setUpCollation(){
let (arrayPersons, arrayTitles) = collation.partitionObjects(array: self.persons, collationStringSelector: #selector(getter: Person.lastName))
self.personsWithSections = arrayPersons as! [[Person]]
self.sectionTitles = arrayTitles
}
With this, I am able able to set up my whole table view and it works great. I get every person sorted up by their last name to their corresponding alphabetical section.
The Issue, Specifically
The problem is that if the user has only inputed their first name and not their last name, that Person object gets appended to the last section of the array, where objects with last names that start with numbers or special characters go.
How can I append these object's with only first names to the corresponding alphabetical
section if the user doesn't input a last name? The same goes with
numbers.
As you can see, my collationsStringSelector takes Person.lastName, I was thinking maybe I could create a computed variable that would return the right person's property under some conditions? I am not sure how I would do so. It would be nice to have some guidance or help!
ps: NEW to the forum! if I'm doing something wrong let me know guys! thanks

Appending to dictionary of [character: [object]] returns 0 key/value pair

I'm trying to show a tableview similar to contacts with my list of users.
I declare a global variable of friends that will store the first character of a name and a list of users whose first name start with that
var friends = [Character: [User]]()
In my fetch method, I do this
for friend in newFriends {
let letter = friend.firstName?[(friend.firstName?.startIndex)!]
print(letter)
self.friends[letter!]?.append(friend)
}
After this, I should have my friends array with the first letter of the name and the users that fall in it; however, my friends dictionary is empty.
How do I fix this?
Edit: I'm following this tutorial and he doesnt exactly the same.. Swift: How to make alphabetically section headers in table view with a mutable data source
Rather than using Character as the key, use String. You need to be sure to init the [User] array for every new First Initial key you insert into groupedNames. I keep an array of groupedLetters to make it easier to get a section count
var groupedNames = [String: [User]]()
var groupedLetters = Array<String>()
func filterNames() {
groupedNames.removeAll()
groupedLetters.removeAll()
for friend in newFriends {
let index = friend.firstName.index(friend.firstName.startIndex, offsetBy: 0)
let firstLetter = String(friend.firstName[index]).uppercased()
if groupedNames[firstLetter] != nil {
//array already exists, just append
groupedNames[firstLetter]?.append(friend)
} else {
//no array for that letter key - init array and store the letter in the groupedLetters array
groupedNames[firstLetter] = [friend]
groupedLetters.append(firstLetter)
}
}
}
Creating a Dictionary structure with Characters as keys & values as Array of User will be more succinct.
The error occurs because you are declaring an empty dictionary, that means you have to add a key / empty array pair if there is no entry for that character.
Consider also to consolidate the question / exclamation marks
class User {
let firstName : String
init(firstName : String) {
self.firstName = firstName
}
}
var friends = [Character: [User]]()
let newFriends = [User(firstName:"foo"), User(firstName:"bar"), User(firstName:"baz")]
for friend in newFriends {
let letter = friend.firstName[friend.firstName.startIndex]
if friends[letter] == nil {
friends[letter] = [User]()
}
friends[letter]!.append(friend)
}

How to remove duplicate data in the UIPickerView in Swift

May I know how to remove the duplicate data which all the data is passing from the Parse database. I just select one column and pass that column data to this picker view.
I've try to find the query.wherekey but it didn't have the DISTINCT function which similar to the SQL statement, so what should I do to avoid the data duplication?
As you add items one by one into your self.pickerString array, just verify if this array contains or not the new value before addObject new value to the array. For example:
if !self.pickerString.contains(object["Intake"] as! String) {
self.pickerString.append(object["Intake"] as! String)
}
There is no built-in function to remove duplicate items in an array in swift (correct me if I'm wrong). However, you can add new features for array with 'extension' keyword:
extension Array {
func distinct<T: Equatable>() -> [T] {
var newArray = [T]()
for item in self {
if !contains(newArray, item as! T) {
newArray.append(item as! T)
}
}
return newArray
}
}
usage:
let s = ["344","333","1","2","333"]
var p = s.distinct() as [String]
I don't think you have anything like distinct on Parse, you'll have to download all rows and then filter out equal values.
For a functional approach:
func distinct<T: Equatable>(source: [T]) -> [T] {
var unique = [T]()
for item in source {
if !contains(unique, item) {
unique.append(item)
}
}
return unique
}
[...]
self.pickerString = distinct(objects)
[...]