Remove duplicate structs in array based on struct property in Swift - ios

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.

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.

Check duplicates properties on Swift array

I have a custom class called Place with 3 properties:
Name (String)
Category (String)
GeoPoint (CLLocationCoordinate2D)
I have an array of type [Place] of 100 objects and I want to check if there are duplicates on the GeoPoint property (just on this one).
How can I check duplicates of a specific property in an array of custom objects?
Thanks!
Although the accepted answer is good, I'd like to chip in.
There are two more ways to achieve what you want, and they both benefit from functionalities provided by SDK.
1 - Use Sets as Tj3n mentioned in a comment.
To achieve this you would need to make your Place conform to Hashable protocol.
class Place : Hashable {
var name = ""
var category = ""
var geoPoint: CLLocationCoordinate2D = CLLocationCoordinate2D()
var hashValue: Int {
get {
return geoPoint.longitude.hashValue &+ geoPoint.latitude.hashValue
}
}
}
func ==(lhs: Place, rhs: Place) -> Bool {
return lhs.geoPoint.latitude == rhs.geoPoint.latitude && lhs.geoPoint.longitude == rhs.geoPoint.longitude
}
The &+ operator in hashValue means "add, and don't crash at overflow". Using it is as straightforward as it can - let set = Set(yourArrayOfPlaces) - the set will contain only unique, in regard to geoPoint, places.
2 - Use KVC. While this is more of an Objective-C world, I find it a useful tool. To achieve this, you'd need to make Place inherit from NSObject. Then getting an array of unique places can be reduced to this one line :
let uniquePlaces = (yourPlacesArray as NSArray).value(forKeyPath: "#distinctUnionOfObjects.geoPoint")
You can do something like this:
var dict : [String : Int] = [:]
for place in arr {
if dict[place.GeoPoint] != nil { // Not in dictionary
if dict[place.GeoPoint] >= 1 { // If already there
return true // Duplicate
} else {
dict[place.GeoPoint]! += 1 // Increment instance
}
} else {
dict[place.GeoPoint] = 0 // Put in dictionary
}
}
return false // No duplicates
Where you loop through a [Place] array and check to see how many have the same GeoPoint. Then check to see if there's one there more than once.

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.

How to achieve object merging through generics in swift?

Imagine I have a class Number:
class Number {
var val: Double?
}
and have two instances of that class, A and B.
Now imagine I want to merge Binto Athrough a statement like
merge(B, into: A)
Now of course I could write the function like this:
func merge(from: Number, into: Number){
into.val = from.val
}
But that isn't reusable at all. Is there a way I could write a generic merge class?
UPDATE: Although some of the answers offer good and viable solutions, none of them are "generic" enough (generic here is meant in a non-technical way).So looking at the answers, I got some inspiration, and here is the solution I am now considering: make Number a NSObject subclass and declare all the properties that can be merged as dynamic. For example:
class Number: NSObject {
//Put the required init and initWithCoder: here
dynamic var val: Double?
}
Then declaring a protocol that mergeable classes must respect
protocol Mergeable: class {
var mergeablePropertyKeys:[String] {get}
}
And then declaring a global function that performs a merge:
func merge<U: Mergeable, Mergeable where U.Type == V.Type>(from: U, into:V){
for property in U.mergeablePropertyKeys {
V.setValue(U.valueForKey(property), property)
}
}
And I know that this will not work because the arguments to merge are not necessarily NSObjects.
How can I make sure that the arguments to merge are both NSObjects?
Can avoid having to specify the names of all my mergeable values by simply obtaining a list of my object's dynamic values?
It sounds like what you want is a generic function that uses reflection to merge properties. Reflection is limited in Swift, but it is doable using the MirrorType. I have used this method before to build a generic json parser in swift - you could do something similar but instead of parsing a json dictionary to properties map your two object's properties.
An example of using reflection to do this in swift:
func merge<T>(itemToMerge:T) {
let mirrorSelf = Mirror(reflecting: self)
let mirrorItemToMerge = Mirror(reflecting: itemToMerge)
for mirrorSelfItem in mirrorSelf.children {
// Loop through items in mirrorItemToMerge.
for mirrorImageItem in mirrorItemToMerge.children {
// If you have a parameter who's name is a match, map the value
// OR You could add any custom mapping logic you need for your specific use case
if mirrorSelfItem.label == mirrorImageItem.label {
// To set values, use self.setValue(valueToSet, forKey: propertyName)
self.setValue(mirrorImageItem.value as? AnyObject, forKey: mirrorImageItem.label!)
}
}
}
}
This assumes the object defining the merge method is a subclass of NSObject (so it can take advantage of NSKeyValueCoding). You could also make this a static method that could merge any 2 objects of any NSObject type:
static func merge<T1: NSObject, T2: NSObject>(itemChanging:T1, itemToMerge:T2) {
let mirrorSelf = Mirror(reflecting: itemChanging)
let mirrorItemToMerge = Mirror(reflecting: itemToMerge)
for mirrorSelfItem in mirrorSelf.children {
// Loop through items in mirrorItemToMerge.
for mirrorImageItem in mirrorItemToMerge.children {
// If you have a parameter who's name is a match, map the value
// OR You could add any custom mapping logic you need for your specific use case
if mirrorSelfItem.label == mirrorImageItem.label {
// To set values, use self.setValue(valueToSet, forKey: propertyName)
self.setValue(mirrorImageItem.value as? AnyObject, forKey: mirrorImageItem.label!)
}
}
}
}
Im not sure what you are expecting but there is generic solution:
class Number<T> {
var val: T?
}
protocol Merge {
func merge(from: Self, into: Self)
}
extension Number: Merge {
func merge(from: Number, into: Number) {
into.val = from.val
}
}
Protocol
Lets define a HasValue protocol (available only for classes) like this
protocol HasValue: class {
typealias T
var val: T? { get set }
}
Merge
Now we can define a generic function
func merge<U: HasValue, V:HasValue where U.T == V.T>(from: U, into:V) {
into.val = from.val
}
The constraints in the function signature do guarantee that
Both params do conform to HasValue (therefore are classes)
val types for both params are equals
Scenario 1: params have the same type
class Number: HasValue {
var val: Double?
}
let one = Number()
one.val = 1
let two = Number()
two.val = 2
merge(one, into: two)
print(two.val) // Optional(1.0)
Scenario 2: params have different types but their values have the same type
I did not constrain the 2 params of Merge to having the same type, I am only checking that the val properties of the 2 params must have the same type.
So we could also merge different instances of different classes having val of the same type like
class Phone: HasValue {
var val: Int?
}
class Computer: HasValue {
var val: Int?
}
let iPhone = Phone()
iPhone.val = 10
let iMac = Computer()
iMac.val = 9
merge(iPhone, into: iMac)
print(iMac.val) // Optional(10)
Scenario 3: params have generic types
class Box<S>: HasValue {
var val: S?
}
let boxOfString = Box<String>()
boxOfString.val = "hello world"
let boxOfInt = Box<Int>()
boxOfInt.val = 12
merge(boxOfString, into: boxOfInt) // << compile error
let boxOfWords = Box<String>()
boxOfWords.val = "What a wonderful world"
merge(boxOfString, into: boxOfWords)
print(boxOfWords.val) // Optional("hello world")

How to use a Set type with custom objects as datasource

I would like to use my
var savings = Set<Saving>()
as Data source in my UITableView.
I can't use something like
cell.name = savings[indexPath.row].name
Because the subscript is only Saving. Is there a way to do this anyway?
I believe what you are looking for is something like:
var savings = Array<Saving>()
and then call it as
cell.name = savings[indexPath.row].name
Update
if you have savings as Set. you can get array out of it and use it like this:
let savingsArray = Array(savings)
and then call it as
cell.name = savingsArray[indexPath.row].name
Set's are inherently unordered, but UITableViewDataSource implementations are required to have a defined ordering. You'll have to convert the set to an Array (which has a defined ordering) and use that to implement the data source methods.
I should first mention that perhaps Set is not the best collection type for you in this case, as---as mentioned by Bill---Set is inherently unordered.
You can, however, access the name property of a Saving instance by matching the indexPath.row integer property to integer id property in your Saving class / structure, making use of Set method .filter(...).
Consider the following example. We first create a Saving structure (or class, as I gather you have) that conforms to Hashable and Equatable protocols, to be eligible as members of collection type Set:
/* Saving: conform to Hashable and Equatable for use as
collection type Set members */
struct Saving : Hashable {
var id: Int
var name: String
/* Make Saving conform to Hashable */
var hashValue: Int {
return self.id
}
init(id: Int, name: String) {
self.id = id
self.name = name
}
}
/* Make Saving conform to Equtable */
func ==(lhs: Saving, rhs: Saving) -> Bool {
return lhs.id == rhs.id
}
Now, consider the following example, of accessing specific members of you Set (of type Saving) given a integer id, e.g. such as corresponding row index (indexPath.row) in your table view:
/* Example */
var savings = Set<Saving>()
savings.insert(Saving(id: 1, name: "Savings A"))
savings.insert(Saving(id: 2, name: "Savings B"))
savings.insert(Saving(id: 3, name: "Savings C"))
let indexPathRow = 1 // for our example
var cellName = savings.filter({ $0.id == indexPathRow }).first?.name ?? "No such Savings id"
print(cellName) // Savings A
Alternatively, create the .name by .id extraction as a function.
func getSavingsName(mySavingsSet: Set<Saving>, myId: Int) -> String {
return mySavingsSet.filter({ $0.id == myId }).first?.name ?? "No such Savings id"
}
/* for your specific example */
cell.name = getSavingsName(savings, myId: indexPath.row)
Preferably handle the case "No such Saving id" using optional return instead, to avoid running into any undetermined behaviour when (trying to) find members of your Set corresponding to row id:s of your table view.

Resources