How to search an Array containing struct elements in Swift? - ios

It's kind pretty straight forward to find an element in an array with type String, Int, etc.
var States = ["CA", "FL", "MI"]
var filteredStates = States.filter {$0 == "FL"} // returns false, true, false
Now, I created a struct
struct Candy{
let name:String
}
and then initialized it
var candies = [Candy(name: "Chocolate"),
Candy(name: "Lollipop"),
Candy(name: "Caramel")]
Can anyone please suggest the right way to find "Chocolate" in the array containing struct elements? I'm not able to implement the find or filter method.

With the following code you receive all candy structs in the array, which match to "Chocolate".
var candiesFiltered = candies.filter{$0.name == "Chocolate"}
If you just want a boolean if it has been found or not you could use the following code:
var found = candies.filter{$0.name == "Chocolate"}.count > 0

Related

Filter array that contains optional - SearchBar

just a bit of hindsight for you to understand my problem. I am currently coding an iOS app for events where I have a GuestList CoreData saved on device.
On viewDidLoad, it will fetch the coredata object and place it into an array of [GuestDetails]
Now the Guestdetail object struct is as follows:
private(set) public var guestFirstName: String
private(set) public var guestLastName: String?
private(set) public var guestEmail: String?
private(set) public var guestPhone: String?
private(set) public var guestUUID: UUID
private(set) public var guestBarcode: String?
private(set) public var guestCheckedIn: Bool
Such that only first name, UUID and checkinStatus are compulsory. I have already set up adding by JSON but now my issue is on my GuestListViewController, I have a searchbar
I am using the following code and array to make sure I can filter.
I have another array that is called
searchResultArray = [GuestDetails]()
So essentially I would copy all my guest details to searchResultArray and that is the one that the tableView is getting its sources from.
Now as part of the search, I used this code which I found on appcoda
searchResultArray = guestData.filter({guestData -> Bool in
(guestData.guestFirstName.lowercased().contains(searchText.lowercased())) ||
(guestData.guestLastName?.lowercased().contains(searchText.lowercased()))! ||
(guestData.guestBarcode?.contains(searchText))!
})
The issue is that my app is crashing, if I only search by first name it will not crash since those are force unwrapped nicely, but if I add the code to search lastName or Barcode, it will crash. I understand that it is probably because of explicitly unwrapped but xcode would not let me NOT unwrap it.
I have tried using map (which does not help, unless i am not familiar enough with it),
I have tried .compact (but I could not get it to work as i am not sure how it can access the inside of an GuestDetail object to remove nils)
The issue is the array of [GuestDetails] itself will not contain null some details inside a GuestDetails object might, hence causing it to crash.
My question is, how do I get it to search by firstname(already possible), lastname and barcode?
Thanks and I hope the question was elaborate enough.
I would avoid using forced unwrapping like so:
searchResultArray = guestData.filter({guestData -> Bool in
let searchLowercased = searchText.lowercased()
if guestData.guestFirstName.lowercased().contains(searchLowercased) {
return true
}
if let guestLastName = guestData.guestLastName, guestLastName.lowercased().contains(searchLowercased) {
return true
}
if let guestBarcode = guestData.guestBarcode, guestBarcode.lowercased().contains(searchLowercased) {
return true
}
return false
})
Answering your question from comments: You don't have to use ? operator on optionals because we use Optional Binding if let syntax. So for example:
if let guestLastName = guestData.guestLastName if guestData.guestLastName is nil than we will just jump out of this if statement. then you see , in the if statement, we will go pass this comma only if guestData.guestLastName is not nil, that is why we can use the guestLastName variable that is unwrapped String and is no longer optional String?, we than proceed to check if search term matches the guestLastName and return true.
Please read: if let , if var, guard let,guard var and defer statements in swift
It would be even better if you happen to add another property to your Data and avoid doing all this if else you can do something like:
searchResultArray = guestData.filter({guestData -> Bool in
let searchLowercased = searchText.lowercased()
let matches:[String?] = [guestData.guestFirstName, guestData.guestLastName, guestData.guestBarcode]
let nonNilElements = matches.compactMap { $0 }
for element in nonNilElements {
if element.lowercased().contains(searchLowercased) {
return true
}
}
return false
})
It's an interesting little problem, so let's generalize it. Here's our test data, comparable to your array of GuestDetails:
struct S {
var s1 : String
var s2 : String?
var s3 : String?
}
var array = [S]()
array.append(S(s1: "test", s2: "yo", s3: "ha"))
array.append(S(s1: "test", s2: nil, s3: nil))
array.append(S(s1: "Howdy", s2: "Bonjour", s3: "Hello"))
let target = "hello"
Some S properties are Optional, others are not.
So the problem is: Filter array down to only those elements where any S property contains our target string, using case insensitive comparison.
We can do that in one statement:
let filteredArray = array.filter {
[$0.s1,$0.s2,$0.s3].compactMap {$0}
.map {$0.localizedCaseInsensitiveContains(target)}
.contains(true)
}
It's not quite as efficient as what #Ladislav wrote, because we keep looping inside map even after we've found our string. But the inefficiency is probably not significant.

How to remove common items from two struct arrays in Swift

In my app I have two struct arrays and I want to remove common items from one of them. My struct:
struct PeopleSelectItem {
var name = ""
var id = ""
var added = false
}
My arrays:
var people : [PeopleSelectItem] = []
var selectedPeople : [PeopleSelectItem] = []
I want to remove items from people array if they exist (compare by id) on selectedPeople array.
I tried several array filtering and converting to set but none of them worked. What can I do here?
Thanks!
Get an array of all ids in selectedPeople
let selectedPeopleIDs = selectedPeople.map(\.id)
Filter the items whose id is not in the array
let filteredPeople = people.filter { !selectedPeopleIDs.contains($0.id) }
If you know that people equal each other if the id is the same then you can conform your struct to the Equatable protocol and you can use the array filter method.
struct PeopleSelectItem : Equatable {
var name = ""
var id = ""
var added = false
}
func ==(lhs: PeopleSelectItem, rhs: PeopleSelectItem) -> Bool {
return lhs.id == rhs.id
}
func filterPeople() {
//swift 2, 3:
people = people.filter{!selectedPeople.contains($0)}
//swift older versions:
people = people.filter{!contains(selectedPeople, $0)}
}
If people might have a significant amount of entries, performance should be considered. So, instead of searching with an n^2 algorithm, you should make use of Swifts dictionary and the corresponding hash-search to find items.
If Id is unique for people then I would store them in a dictionary like:
var peopleDict: [String:PeopleSelectItem] = [:]()
You can easily convert from the array you have to this dictionary:
people.foreach {peopleDict[$0.id] = $0}
With this dictionary it's very easy to delete single entries:
selectedPeople.foreach {peopleDict.remove($0.id)}
Optionally to switch back to an array for people you just say:
let filteredPeople = peopleDict.values as [PeopleSelectItem]
Remarks
I assumed, that selectedPeople is smaller then the base of all people. If this is not the case, you should pu selectedPeople in a dictionary.
did I say I like this Spark like api? I think I do so.
I just wrote that code from top of my mind. If it is not completely syntactically correct let me know and I correct it.

Remove duplicate structs in array based on struct property in Swift

I have made a simple struct and implemented the Equatable protocol :
extension MyModelStruct: Equatable {}
func ==(lhs: NModelMatch, rhs: NModelMatch) -> Bool {
let areEqual = lhs.id == rhs.id
return areEqual
}
public struct MyModelStruct {
var id : String?
var staticId : String?
init(fromDictionary dictionary: NSDictionary){
id = dictionary["id"] as? String
...
}
Then in my project i get an array of [MyModelStruct], what i what to do is to remove all the MyModelStruct that have the same id
let val1 = MyModelStruct(id:9, subId:1)
let val2 = MyModelStruct(id:10, subId:1)
let val3 = MyModelStruct(id:9, subId:10)
var arrayOfModel = [val1,val2,val3]; // or set but i do not know how to use a set
var arrayCleaned = cleanFunction[M2,M3]
How can i make the cleanFunction ?
Can someone help please.
Thanks for all.
Xcode : Version 7.3.1
I really don't want people to just take an answer because it's the only one, that's why I'm showing you how you can use the power of sets. Sets are used wherever it doesn't make sense to have more than one, either it's there or not. Sets provide fast methods for checking whether an element is in the set (contains), removing an element (remove), combining two sets (union) and many more. Often people just want an array because they're familiar with it, but often a set is really what they need. With that said, here is how you can use a set:
struct Model : Hashable {
var id : String?
var hashValue: Int {
return id?.hashValue ?? 0
}
}
func ==(l: Model, r: Model) -> Bool {
return l.id == r.id
}
let modelSet : Set = [
Model(id: "a"),
Model(id: "hello"),
Model(id: "a"),
Model(id: "test")
]
// modelSet contains only the three unique Models
The only requirement for a type in order to be in a set is the Hashable protocol (which extends Equatable). In your case, you can just return the underlying hashValue of the String. If your id is always a number (which it probably is), you should change the type of id to be an Int, because Strings are much less efficient than Ints and it doesn't make sense to use a String.
Also consider storing this property somewhere, so that every time you receive new models, you can just do
modelSet.unionInPlace(newModels)
Use a Set instead of an Array.
I agree you are better off using a Set. You should be able to initialize the Set using an Array, e.g., var arrayOfModel: Set = [val1, val2, val3]. But because you are using a custom type, you will need to make sure that MyModelStruct conforms to hashable. This link has a good explanation.
But if you want to use an array then you need to change
let areEqual = rhs.id == lhs.id
to
let areEqual = rhs.id == lhs.id && rhs.subId == lhs.subId)
You need to modify your struct to have a subId property (and make the variables Int instead of String.
In answer to your question, yes you do need to iterative over the array.
If you extend the Array type with this function :
extension Array
{
func uniqueValues<V:Equatable>( value:(Element)->V) -> [Element]
{
var result:[Element] = []
for element in self
{
if !result.contains({ value($0) == value(element) })
{ result.append(element) }
}
return result
}
}
You'll be able to clean up duplicates using :
var arrayCleaned = arrayOfModel.uniqueValues({$0.id!})
without needing to make the structure Equatable.
Please note that this is not going to be efficient if your array is very large and you might want to consider filtering insertions into your array at the source if possible.

Swift3: Using Two Arrays to populate grouped Table

I currently have two populated arrays using a custom struct.
struct Group {
var id: String
var type: String
var desc: String
var name: String
init() {
id = ""
type = ""
desc = ""
name = ""
}
}
Data gets appended to:
var clientArray: [Group] = []
var departmentArray: [Group] = []
I essentially want to join them together to have the format something like [[clientArray], [departmentArray]] so I can use "section" and populate two different groups on a table with the respective arrays.
So far I've tried the following, but I get the error "fatal error: index out of range".
var masterArray = [[Group]]()
//Then further down the page...
self.masterArray[0] = self.clientArray
self.masterArray[1] = self.departmentArray
How can I get this to work? Thanks for any help.
You could write:
var masterArray = [self.clientArray, self.departmentArray]
Otherwise use append:. The docs state:
You can’t use subscript syntax to append a new item to the end of an
array.

tell an array to hold instances of an array?

lets say I have
struct Two {var names: String }
then I create two instances of this struct
var one = Two(names: "one")
var two = Two(names: "two")
Would I be able to create an array that specifically holds struct instances, something like?
var options:Two = [[one],[two]]
If not, what would be the advisable path to complete this logic?
Yes, you can. The type you're looking for is [[Two]], which is a 2D array of Two, or an array of arrays of Twos. Usage:
struct Two {
var names: String
}
var one = Two(names: "one")
var two = Two(names: "two")
var options: [[Two]] = [[one],[two]]

Resources