Filtering/Searching Nested Struct Array - ios

I'm trying to add a search bar to the top of a grouped table. However I'm unsure how to filter through my data. The data is stored in a nested array with objects held in this format;
struct Group {
var id: String
var type: String
var desc: String
var avatar: String
var name: String
init() {
id = ""
type = ""
desc = ""
avatar = ""
name = ""
}
}
Because I get data from two sources, two arrays are nested together, this also makes it simpler to create the two sections of the grouped table. I'll note, they both use the same Group struct.
self.masterArray = [self.clientArray, self.departmentArray]
This "masterArray" is then used to populate the table. Filtering/searching a single array isn't too difficult, but how do I search through a nested array?
func searchBar(searchBar: UISearchBar, textDidChange searchText: String) {
}
EDIT:
I've finally got things working, courtesy of #appzYourLife.
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
print("Searching for:" + searchText)
if searchText.isEmpty {
filterArray = masterArray
} else {
filterArray = [ clientArray.filter { $0.name.range(of: searchText) != nil }] + [ departmentArray.filter { $0.name.range(of: searchText) != nil } ]
}
tableView.reloadData()
}

You can make use of .flatten() to flatten your array prior to filtering it for whatever search criteria you want to use. E.g.
struct Group {
var id: String
var name: String
init(_ id: String, _ name: String) { self.id = id; self.name = name }
/* .... */
}
let clientArray = [Group("a", "John"), Group("b", "Jane"), Group("c", "Phil")]
let departmentArray = [Group("a", "Foo"), Group("b", "Bar"),
Group("c", "Baz"), Group("d", "Bax")]
let arr = [clientArray, departmentArray]
// find some id
let searchForId = "c"
let hits = arr.flatten()
.filter { $0.id.lowercaseString.containsString(searchText.lowercaseString) }
print(hits)
// [Group(id: "c", name: "Phil"), Group(id: "c", name: "Baz")]
From the edits of your questions, it seems, however, that you want the resulting filtered array to be of the same nested array type as the "master" array. In such case, the following is a more appropriate approach:
/* ... */
// find some id but keep [[Group]] type
let searchText = "c"
let hits = arr.map { $0.filter { $0.id.lowercaseString.containsString(searchText.lowercaseString) } }
print(hits)
// [[Group(id: "c", name: "Phil")], [Group(id: "cc", name: "Baz")]]

Given
struct Group {
let id: String = ""
let type: String = ""
let desc: String = ""
let avatar: String = ""
let name: String = ""
}
let clients = [Group(), Group(), Group(), Group()]
let departmens = [Group(), Group(), Group(), Group()]
let clientsAndDepartments = [clients, departmens]
You can search inside clients and department writing
let results = (clients + departmens).filter { $0.id == "123" }
Update #1
Now understand that you want to filter both arrays but as result you still want something like this [[Group]].
So here's the code
var filterArray = [clients.filter { $0.name == "White" }] + [departmens.filter { $0.name == "White" }]
Update #2
If you want to search for string inclusione the use this code
var filterArray = [ clients.filter { $0.name.rangeOfString("White") != nil }] + [ departmens.filter { $0.name.rangeOfString("White") != nil } ]

You can map over each of the arrays, and filter them independently:
self.masterArray.map{ subarray in
subarray.filter { element in
trueWhenElementShouldStay(element)
}
}
P.S. I suspect masterArray should NOT be an instance variable, it would be more appropriate as a local variable instead.

Related

Dictionary of Dictionaries of Dictionaries of Dictionaries syntax understanding

I need to map the doors of buildings into a single map, from which afterward I need to populate three pickers using this data.
Each door contains the following data: building, level, range, door number and other information that is less relevant. So I created the following map:
public var doorsMap: [String : [String : [String : [String: Door]]]] = [:]
and a have a list of doors that I need to populate this map with, the problem is that I can't understand what should be the right syntax to perform this task, I tried:
doorsMap[door.building]?[door.level]?[door.range]?[door.number] = door
but this doesn't create the inner sets of dictionaries. when I tried to do:
doorsMap[door.building]![door.level]![door.range]![door.number] = door
Obviously, I get the:
Fatal error: Unexpectedly found nil while unwrapping an Optional value
because I try to unwrap a nil value.
So what would be the correct syntax in swift to populate this map from a list of doors?
A single assignment won't create the multiple, intermediate directories. You need to do it explicitly.
You could use something like this:
func add(door: Door) {
var building = self.doorsMap[door.building] ?? [String : [String:[String: Door]]]()
var level = building[door.level] ?? [String : [String: Door]]()
var range = level[door.range] ?? [String:Door]
range[door.number] = door
level[door.range] = range
building[door.level] = level
self.doorsMap[door.building] = building
}
Personally, I would look for a better data structure, perhaps use a struct to hold the doorsMap. This struct could have functions to handle the insertion and retrieval of doors.
Perhaps something like this:
struct Door {
let building: String
let level: String
let range: String
let number: String
}
struct DoorMap {
private var buildingsSet = Set<String>()
private var levelsSet = Set<String>()
private var rangesSet = Set<String>()
private var numberSet = Set<String>()
private var doorsArray = [Door]()
var buildings: [String] {
get {
return Array(buildingsSet).sorted()
}
}
var levels: [String] {
get {
return Array(levelsSet).sorted()
}
}
var ranges: [String] {
get {
return Array(rangesSet).sorted()
}
}
var numbers: [String] {
get {
return Array(numberSet).sorted()
}
}
var doors: [Door] {
get {
return doorsArray
}
}
mutating func add(door: Door) {
buildingsSet.insert(door.building)
levelsSet.insert(door.level)
rangesSet.insert(door.range)
numberSet.insert(door.number)
doorsArray.append(door)
}
func doorsMatching(building: String? = nil, level: String? = nil, range: String? = nil, number: String? = nil) -> [Door]{
let matches = doorsArray.filter { (potentialDoor) -> Bool in
var included = true
if let b = building {
if potentialDoor.building != b {
included = false
}
}
if let l = level {
if potentialDoor.level != l {
included = false
}
}
if let r = range {
if potentialDoor.range != r {
included = false
}
}
if let n = number {
if potentialDoor.number != n {
included = false
}
}
return included
}
return matches
}
}
var map = DoorMap()
let d1 = Door(building: "b1", level: "1", range: "r1", number: "1")
let d2 = Door(building: "b1", level: "2", range: "r1", number: "2")
let d3 = Door(building: "b2", level: "2", range: "r1", number: "2")
map.add(door: d1)
map.add(door: d2)
map.add(door: d3)
let b1Doors = map.doorsMatching(building:"b1")
let level2Doors = map.doorsMatching(level:"2")
let allBuildings = map.buildings()
Now, maybe you have more information on buildings and levels etc, so they could be structs too instead of just strings.

how to apply filter on inside array in array of dictionary?

I have a situation like i have array of dictionaries, in which each key has array of specific object. like this.
// example setup
struct FruitsData {
var name: String?
var id: String?
}
tableViewSource = [String : [FruitsData]]
so i have to apply filter on this inner array. but i am unable to update the value in final array.
I have written this code.
tableViewSource = tableViewSource.filter { ( dictData: (key: String, value: [FruitsData])) -> Bool in
var arrFruitsData = dictData.value
arrFruitsData = arrFruitsData.filter{ ( $0.id != nil) }
if arrFruitsData.count == 0 {
self.tableViewHeaders = self.tableViewHeaders.filter { $0 != dictData.key }
}
return true
}
like i have remove those values in array whose id has been removed.
for eg if i have these values in array.
var array = ["A": [FruitsData(name: "apple", id: "5"), FruitsData(name: "apricot",id: "")], "M": [FruitsData(name: "mango", id: "9"), FruitsData(name: "grapes", id: "")]]
First of all tableViewSource is not the Array of Dictionary, it is Dictionary with each key having Array as value. Also from you example id is not nil but empty if you want remove object of FruitsData whose id is nil or empty you can make it like this.
var tableViewSource = [String : [FruitsData]]()
tableViewSource.forEach {
let res = $1.filter { $0.id != nil && $0.id != "" }
tableViewSource[$0] = res
}

Filter searchText to tableView

Right now I am using following
let data = ["New York, NY", "Los Angeles, CA" . . .]
var filteredData: [String]!
filteredData = data
But I want to use Firebase, with an almost identical structure, by using this
var data = [Categories]()
(This is categories)
struct Categories {
let key:String!
let content:String!
let itemRef:FIRDatabaseReference?
init (content:String, key:String = "") {
self.key = key
self.content = content
self.itemRef = nil
}
init (snapshot:FIRDataSnapshot) {
key = snapshot.key
itemRef = snapshot.ref
if let CategoriesContent = snapshot.value!["content"] as? String {
content = CategoriesContent
} else {
content = ""
}
}
}
So that when I search for something these lines is supposed to filter out everything that aren't correct
func searchBar(searchBar: UISearchBar, textDidChange searchText: String) {
// Unhide tableview, this will be changed to another method
tableView.hidden = false
filteredData = searchText.isEmpty ? data : data.filter({(dataString: String) -> Bool in
// If dataItem matches the searchText, return true to include it
return dataString.rangeOfString(searchText) != nil
})
tableView.reloadData()
}
But since filter({(dataString: String) only takes strings it does not work
Question : Is there any other way to replace the string with my Firebase struct?
Thanks a lot!
this tutorial is so clear in UISearchResultsUpdating and Filtering section.

Swift 3: sum value with group by of an array of objects

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

How to access property or method from a variable?

Is it possible to access a method or property using a variable as the name of the method or property in Swift?
In PHP you can use $object->{$variable}. For example
class Object {
public $first_name;
}
$object = new Object();
$object->first_name = 'John Doe';
$variable = 'first_name';
$first_name = $object->{$variable}; // Here we can encapsulate the variable in {} to get the value first_name
print($first_name);
// Outputs "John Doe"
EDIT:
Here is the actual code I'm working with:
class Punchlist {
var nid: String?
var title: String?
init(nid: String) {
let (result, err) = SD.executeQuery("SELECT * FROM punchlists WHERE nid = \(nid)")
if err != nil {
println("Error")
}
else {
let keys = self.getKeys() // Get a list of all the class properties (in this case only returns array containing "nid" and "title")
for row in result { // Loop through each row of the query
for field in keys { // Loop through each property ("nid" and "title")
// field = "nid" or "title"
if let value: String = row[field]?.asString() {
// value = value pulled from column "nid" or "title" for this row
self.field = value //<---!! Error: 'Punchlist' does not have a member named 'field'
}
}
}
}
}
// Returns array of all class properties
func getKeys() -> Array<String> {
let mirror = reflect(self)
var keys = [String]()
for i in 0..<mirror.count {
let (name,_) = mirror[i]
keys.append(name)
}
return keys
}
}
You can do it, but not using "pure" Swift. The whole point of Swift (as a language) is to prevent that sort of dangerous dynamic property access. You'd have to use Cocoa's Key-Value Coding feature:
self.setValue(value, forKey:field)
Very handy, and it crosses exactly the string-to-property-name bridge that you want to cross, but beware: here be dragons.
(But it would be better, if possible, to reimplement your architecture as a dictionary. A dictionary has arbitrary string keys and corresponding values, and thus there is no bridge to cross.)
Subscripting may help you.
let punch = Punchlist()
punch["nid"] = "123"
println(punch["nid"])
class Punchlist {
var nid: String?
var title: String?
subscript(key: String) -> String? {
get {
if key == "nid" {
return nid
} else if key == "title" {
return title
}
return nil
}
set {
if key == "nid" {
nid = newValue
} else if key == "title" {
title = newValue
}
}
}
}

Resources