func == from equatable protocol does not work for custom object, swift - ios

My goal is to show a user list of history logins ( such as username ) if there are any. In order to do that, I am doing
1. Create an custom object named User like below
class User: NSObject
{
var login: String
init(login: String)
{
self.login = login
}
required init(coder aDecoder: NSCoder) {
login = aDecoder.decodeObjectForKey("login") as! String
}
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(login, forKey: "login")
}
}
// This conform to make sure that I compare the `login` of 2 Users
func ==(lhs: User, rhs: User) -> Bool
{
return lhs.login == rhs.login
}
At UserManager, Im doing save and retrieve an User. Before saving, I'm doing a check if the the list of history logins contains a User, I wont add it in, otherwise.
class UserManager : NSObject
{
static let sharedInstance = UserManager()
var userDefaults = NSUserDefaults.standardUserDefaults()
func saveUser(user:User)
{
var users = retrieveAllUsers()
// Check before adding
if !(users.contains(user))
{
users.append(user)
}
let encodedData = NSKeyedArchiver.archivedDataWithRootObject(users)
userDefaults.setObject(encodedData, forKey: "users")
userDefaults.synchronize()
}
func retrieveAllUsers() -> [User]
{
guard let data = userDefaults.objectForKey("users") as? NSData else
{
return [User]()
}
let users = NSKeyedUnarchiver.unarchiveObjectWithData(data) as! [User]
// Testing purpose
for user in users
{
print(user.login)
}
return users
}
}
At first time trying, I do
UserManager.sharedInstance.saveUser(User(login: "1234"))
Now it saves the first login. At second time, I also do
UserManager.sharedInstance.saveUser(User(login: "1234"))
UserManager still adds the second login into nsuserdefault. That means the function contains fails and it leads to
func ==(lhs: User, rhs: User) -> Bool
{
return lhs.login == rhs.login
}
does not work properly.
Does anyone know why or have any ideas about this.

The problem is that User derives from NSObject. This means that (as you rightly say) your == implementation is never being consulted. Swift's behavior is different for objects that derive from NSObject; it does things the Objective-C way. To implement equatability on an object that derives from NSObject, override isEqual:. That is what makes an NSObject-derived object equatable in a custom way, in both Objective-C and Swift.
Just paste this code right into your User class declaration, and contains will start working as you wish:
override func isEqual(object: AnyObject?) -> Bool {
if let other = object as? User {
if other.login == self.login {
return true
}
}
return false
}

What's going on?
As #matt already said, the problem is about equality.
Look
var users = [User]()
users.append(User(login: "1234"))
users.contains(User(login: "1234")) // false
Look again
var users = [User]()
let user = User(login: "1234")
users.append(user)
users.contains(user) // true <---- THIS HAS CHANGED
contains
The contains function is NOT using the logic you defined here
func ==(lhs: User, rhs: User) -> Bool {
return lhs.login == rhs.login
}
Infact it is simply comparing the memory addresses of the objects.
Solution
You can solve the issue passing your own logic to contains, just replace this
if !(users.contains(user)) {
users.append(user)
}
with this
if !(users.contains { $0.login == user.login }) {
users.append(user)
}

Related

Realm Swift duplicates item with primary key -> Threading problem?

I'm using ReamSwift to store data in my iOS application and I have troubles to figure out, why sometimes objects are duplicated in my application.
I use an UUID as primary key on every object. This property is defined in a base class and all the others are subclasses of that.
I use the update approach with a dictionary. So new objects are created and existing should be updated.
I use a central class which holds the Realm object and handles the update. This method could be called from multiple different threads.
This is my base class
import RealmSwift
open class RealmObject : Object {
static let ID_ATTRIBUTE : String = "id"
#objc dynamic var id : String = ""
override public static func primaryKey() -> String? {
return "id"
}
}
Central class managing swift.
This class holds the Realm Object
public var realm: Realm {
var tempRealm: Realm?
do {
tempRealm = try Realm(configuration: configuration)
}
catch let err {
print("------ERROR----- initializing realm\n \(err)")
}
if let tempRealm = tempRealm{
return tempRealm
}
return self.realm
}
This is the update method in the class. As a fallback if the id property is not set, it will be set as it is the primary key
func update<T: RealmObject>(_ obj : T, values : [String:Any?] ) -> T? {
var vals = values
do {
var res : T?
try realm.write {
if let idattr = vals[T.ID_ATTRIBUTE] as? String {
if(idattr.isEmpty) {
vals[T.ID_ATTRIBUTE] = UUID().uuidString
}
} else {
vals[T.ID_ATTRIBUTE] = UUID().uuidString
}
res = realm.create(T.self, value: vals, update: .modified)
}
return res
} catch {
return nil
}
}
Could calling the update method cause in any case the duplication as I set the primary key of the object? The problem is, I cannot reproduce to find the problem, i just encounter the issue from time to time and in the field from users.
One interesting thing, when a copy is deleted, also the other object will be deleted.
This is the method which deletes objects by id and type
func delete<T: RealmObject>(_ id : String, _ type : T.Type) -> Bool {
do {
let obj = get(id, T.self)
if let obj = obj {
try realm.write {
realm.delete(obj)
}
return true
} else {
return false
}
} catch {
return false
}
}

Add / Update custom object dictionaries array in swift 4

I have an array of dictionary with custom object in swift.
Now I am comparing the object for add & update.
The logic is as simple to add the data if not exist and update if any change in dictionary.
User is custom object type:
#objc public class User: NSObject , Mappable
from the getUserID i can able to get userID
The below code is execute in for loop from where i am passing User object.
var peopleList = [User]()
if self.peopleList.count > 0 {
if self.peopleList.contains(where: {$0.getUserID() == users.getUserID()})
{
// check for any update in dist
if let index = self.peopleList.index(of: users)
{
if users.isEqual(self.peopleList[index])
{
print("equal no updates")
}
else
{
print("need to updates objects..")
}
}
//already exist room
}
else
{
self.peopleList.append(users)
}
}
I know it may be related to equatable
so I am using below fuction
func isEqual<T: Equatable>(type: T.Type, a: Any, b: Any) -> Bool? {
guard let a = a as? T, let b = b as? T else { return nil }
return a == b
}
But I am getting index = nil.
Is there any idea or suggestion to solve it.
If any other way to do it efficiently them most welcome.
I think this simplified version should work
if self.peopleList.isEmpty, let user = self.peopleList.first(where: { $0.getUserID() == users.getUserID() }) {
if user == users {
// is equal
} else {
// do update
}
} else {
self.peopleList.append(users)
}

Dealing with Firebase observers when userID changes?

I use this function inside an helper class to fetch the trips of a user. It keeps listening for new changes:
import Foundation
import Firebase
class APICardService: NSObject {
class func fetchTrips(forID userID: String, completion: #escaping ([Trip]) -> Void) {
let path = "users/\(userID)/trips"
Database.database().reference().child(path).queryOrdered(byChild: "timestamp").observe(.value) { (snapshot) in
var trips = [Trip]()
for child in snapshot.children {
if let child = child as? DataSnapshot {
if let dict = child.value as? [String: Any] {
let trip = Trip(key: child.key, dbValues: dict)
trips.append(trip)
}
}
}
completion(trips)
}
}
}
class MyViewController: UIViewController {
// but what if the userID changes? I have no way to stop the current observer and start a new one
internal func fetchCards() {
guard let userID = APIAuthService.getUserID() else { return }
APICardService.fetchTrips(forID: userID) { trips in
self.trips = trips.reversed() // show most recent first
self.addAnnotations(for: trips)
self.collectionView.reloadData()
}
}
}
The problem is that whenever the user signs out and logs into a new account this observer will still be listening for new value changes in the data but it won't work because the userID changed. How do I deal with this? I could remove the observer somehow but this is complicated because we're inside a separate class and I wouldn't know how to handle that.
In your class APICardService create these global variables
static var refHandle:DatabaseHandle? = nil
static var ref:DatabaseReference? = nil
in your Method func fetchTrips(forID userID: String, completion: #escapin
assign global variables like this
let path = "users/\(userID)/trips"
ref = Database.database().reference().child(path)
refHandle = ref?.queryOrdered(byChild: "timestamp").observe(.value) { (snapshot) in
Now you have the reference of your observer
just add following method in your class
class func removeObserverForFetchTrip() {
if let refHandleValue = refHandle {
ref?.removeObserver(withHandle: refHandleValue)
}
}
When user logout just call this method
Hope it is helpful
As #PrashantTukadiya says.
Try making something like this:
struct User {
var id: String
...
}
class UserHolder {
static var shared: UserHolder = UserHolder() // Singleton
var currentUser: User? {
willSet {
// Remove all the registered firebase handlers associated with the user
firebaseHandles.forEach({ $0.0.removeObserver(withHandle: $0.1) })
}
}
var firebaseHandles: [(DatabaseReference, UInt)] = []
// Note: Try to add all the firebase handles you register to "firebaseHandles"
// array. The more adequate solution is to make this class register
// the listeners, not all other classes.
}

How to access variable from NSObject class?

I'm trying to learn mvc design pattern in swift. So I made the model class named User like below :
class User: NSObject {
var email : String!
var password : String!
var profilePictureUrl : String!
init(email: String, password: String, profilePictureUrl: String) {
super.init()
self.email = email
self.password = password
self.profilePictureUrl = profilePictureUrl
}}
and I'm using another class that store the function named loginConnection:
class loginConnection: NSObject {
class func loginUserWithEmailPassword(email: String,password: String) -> User{
return User(email: email, password: password, profilePictureUrl: "nil")
}}
And I try to set and get the email,password, and profilePictureUrl from my loginViewController but I always get nil when I print the User object.
var userObj : User!
#IBAction func loginAction(sender: UIButton) {
if userEmailTextField.text?.isEmpty != nil && userPasswordTextField.text?.isEmpty != nil{
loginConnection.loginUserWithEmailPassword(userEmailTextField.text!, password:userPasswordTextField.text!)
}
}
#IBAction func registerAction(sender: UIButton) {
print("\(userObj.email) >>>>> \(userObj.password)")
}
How can I access variable from User class?
Change your loginAction method as below,
#IBAction func loginAction(sender: UIButton) {
if userEmailTextField.text?.isEmpty == false && userPasswordTextField.text?.isEmpty == false {
self.userObj = loginConnection.loginUserWithEmailPassword(userEmailTextField.text!, password:userPasswordTextField.text!)
print("\(userObj.email) >>>>> \(userObj.password)")
}
}
1) you were comparing userEmailTextField.text?.isEmpty with nil, isEmpty returns Bool value.
2) you were not assigning value of type User returned by the function loginUserWithEmailPassword.
so then you have to do the following:-
var userObj : User = User()
userObj = loginConnection.loginUserWithEmailPassword(userEmailTextField.text!, password:userPasswordTextField.text!)
after that
userObj.email
Are you calling userObj from loginAction?
Like Below..
var userObj : User!
#IBAction func loginAction(sender: UIButton) {
if userEmailTextField.text?.isEmpty != nil && userPasswordTextField.text?.isEmpty != nil{
userObj = loginConnection.loginUserWithEmailPassword(userEmailTextField.text!, password:userPasswordTextField.text!)
print("\(userObj.email) >>>>> \(userObj.password)")
}
}
loginUserWithEmailPassword return User class object so you can use that to access User class properties
I can't see why this needs to be an NSObject. By making it a struct you can remove the init since it will come automatically. Also remove the ! in most cases since it quite dangerous to use implicitly unwrapped optionals unless you know what you're doing. Xcode will also help with the autocompletion and give good suggestions on how to fix as you go. If you do this you'll find the compiler tell you about the problems before run time errors can happen.
You declared userObj instance of User but you didn't assigned it by the values will return from loginUserWithEmailPassword function.
Assign you userObj as below in your viewController loginAction.
#IBAction func loginAction(sender: UIButton) {
userObj = loginConnection.loginUserWithEmailPassword("Karan", password:"karanPassword")
self.registerAction()
}
Now you will get the assigned username and password.
#IBAction func registerAction(sender: UIButton) {
print("\(userObj.email) >>>>> \(userObj.password)")
}
Here I got the username and password on my output window

store Array containing CGPoints in CoreData database (Swift)

so as the title already states I'm trying to save an array to the database. If this is possible, how do I do it? If not I hope you can help me with some other solution.
I am making an iOS app where if the user touches (and moves) the screen I store this data in an array. Because it needs to be multi-touch all the CGPoints of the touches (either touchesBegan or touchesMoved) on one moment are stored in an array, which again is stored in the main array. Resulting in var everyLocation = [[CGPoint]](). I already found out that it's not possible to store a CGPoint in a database directly, so I can convert them to string with NSStringFromCGPoint(pointVariable). This however isn't very useful as long as I can't store the array...
I want to store the date on which it happened too, so in my database I created the entity 'Locations' with two attributes: 'locations' and 'date'. In the final application the entity name will be the name of the exercise the user was doing (I have about four exercises, so four entities).
Most of the sample code I've seen stores the CGPoint either in a separate x and y or in one string. I can maybe do this too, so I don't have to store arrays. To do this I think I will have to make the attribute(s) the coordinates of the touche(s), the entity name would be the date, and the db name would be the name of the exercise. If this is the only solution, how do I create an entity (with attributes) at run-time?
Thanks in advance
Swift3 makes it seamless,
just write
typealias Point = CGPoint
and set the attribute type to Transformable and set the Custom class of it to
Array<Point>
Works for me without having to do anything.
1) add a "Transformable" type attribute.
2) Event.h
#interface Event : NSManagedObject
#property (nonatomic, retain) NSArray * absentArray;
#interface AbsentArray : NSValueTransformer
#end
Event.m
#implementation AbsentArray
+ (Class)transformedValueClass
{
return [NSArray class];
}
+ (BOOL)allowsReverseTransformation
{
return YES;
}
- (id)transformedValue:(id)value
{
return [NSKeyedArchiver archivedDataWithRootObject:value];
}
- (id)reverseTransformedValue:(id)value
{
return [NSKeyedUnarchiver unarchiveObjectWithData:value];
}
#end
3) Just use it as a normal array
Event *event = //init
event.absentArray = #[1,2,3];
[context save:nil]
Just change these code in swift.
You can understand as .swfit combine .h/.m file. Objective C has .h as header file which many properties there. .m is implication file which methods should be there.
For example:
.swift
import Foundation
import CoreData
class Event: NSManagedObject {
#NSManaged var absentArray: AnyObject
}
3) save:
let appDelegate =
UIApplication.sharedApplication().delegate as AppDelegate
let managedContext = appDelegate.managedObjectContext!
if !managedContext.save(&error) {
println("Could not save \(error), \(error?.userInfo)")
}
I finally managed to put the pieces together after William pointed me in the direction of transformables. I used this tutorial to understand how to work with this: http://jamesonquave.com/blog/core-data-in-swift-tutorial-part-1/
Here are the things I learned from going through this exercise that was prompted by the warning message:
At some point, Core Data will default to using "NSSecureUnarchiveFromData" when nil is specified, and transformable properties containing classes that do not support NSSecureCoding will become unreadable.
My app had collected the series of points [CGPoint] created by drawing on the screen with an Apple Pencil or finger and stored that in CoreData - basically the heart of a thing I called a Scribble. To store in CoreData, I created an attribute named “points” and set the type to Transformable. The Custom Class was set to [CGPoint]. Also, I set CodeGen to Manual rather than the automatic “Class Definition” option. When I generated the CoreData managed object subclass files, it generates a +CoreDataClass.swift file with the critical line of interest being:
#NSManaged public var points: [CGPoint]?
It should be noted, that there is actually a problem if you use the automatic option as the file that is generated doesn’t know what a CGPoint is and cannot be edited to add the import for UIKit for it to find the definition.
This worked fine until Apple started wanting to encourage secure coding. In the code file below, I developed a ScribblePoints object to work with the encoding and its associated data transformer.
//
// ScribblePoints.swift
//
import Foundation
import UIKit
public class ScribblePoints: NSObject, NSCoding {
var points: [CGPoint] = []
enum Key: String {
case points = "points"
}
init(points: [CGPoint]) {
self.points = points
}
public func encode(with coder: NSCoder) {
coder.encode(points, forKey: Key.points.rawValue)
}
public required convenience init?(coder: NSCoder) {
if let sPts = coder.decodeObject(of: ScribblePoints.self, forKey: Key.points.rawValue) {
self.init(points: sPts.points)
} else {
return nil
}
}
}
extension ScribblePoints : NSSecureCoding {
public static var supportsSecureCoding = true
}
#available(iOS 12.0, *)
#objc(ScribblePointsValueTransformer)
final class ScribblePointsValueTransformer: NSSecureUnarchiveFromDataTransformer {
static let name = NSValueTransformerName(rawValue: String(describing: ScribblePointsValueTransformer.self))
override static var allowedTopLevelClasses: [AnyClass] {
return [ScribblePoints.self]
}
public static func register() {
let transformer = ScribblePointsValueTransformer()
ValueTransformer.setValueTransformer(transformer, forName: name)
}
override class func allowsReverseTransformation() -> Bool {
return true
}
override func transformedValue(_ value: Any?) -> Any? {
if let data = value as? Data {
// Following deprecated at iOS12:
// if let data = value as? Data {
// if let points = NSKeyedUnarchiver.unarchiveObject(with: data) as? [CGPoint] {
// return points
// }
// }
do {
let unarchiver = try NSKeyedUnarchiver(forReadingFrom: data)
unarchiver.requiresSecureCoding = false
let decodeResult = unarchiver.decodeObject(of: [NSArray.self, ScribblePoints.self], forKey: NSKeyedArchiveRootObjectKey)
if let points = decodeResult as? [CGPoint] {
return points
}
} catch {
}
}
return nil
}
override func reverseTransformedValue(_ value: Any?) -> Any? {
if let points = value as? [CGPoint] {
// Following deprecated at iOS12:
// let data = NSKeyedArchiver.archivedData(withRootObject: points)
// return data
do {
let data = try NSKeyedArchiver.archivedData(withRootObject: points, requiringSecureCoding: true)
return data
} catch {
}
}
return nil
}
}
With the above in place, I could finally fill in ScribblePointsValueTransformer for the Transformer name for the “points” attribute in CoreData.
One can also switch the Custom Class from [CGPoint] to ScribblePoints. This doesn’t appear to affect code execution. However, if you re-generate the +CoreDataClass.swift file, the critical line of interest will become:
#NSManaged public var points: ScribblePoints?
and when you re-compile you will have code changes to make to deal with the different definition. If you were starting from scratch, it seems you may want to simply use the ScribblePoints definition, and avoid the hassles of dealing with NSArrays and NSPoints and other stuff you magically encounter in strange ways with [CGPoint].
Above was with Swift 5.
Ran into a warning message with my answer above when I hooked up an older iOS device (iOS9) to Xcode. Things worked, but the warning message about not finding the value transformer was disturbing. The problem was that the previous answer only defined and registered the value transformer if you were on iOS12+. To work without complaint on earlier systems, one needs to avoid the NSSecureUnarchiveFromDataTransformer, use ValueTransformer instead, and rely on the NSSecureCoding conformance for your coding object. Then you can register your value transformer on older iOS systems. It should also be noted that the transformedValue() and reverseTransformedValue() functions became reversed.
The net result is the following code instead.
//
// ScribblePoints.swift
//
import Foundation
import UIKit
public class ScribblePoints: NSObject, NSCoding {
var points:[CGPoint] = []
enum Key: String {
case points = "points"
}
init(points: [CGPoint]) {
self.points = points
}
public func encode(with coder: NSCoder) {
coder.encode(points, forKey: Key.points.rawValue)
}
public required convenience init?(coder: NSCoder) {
if let sPts = coder.decodeObject(of: ScribblePoints.self, forKey: Key.points.rawValue) {
self.init(points: sPts.points)
} else {
return nil
}
}
}
extension ScribblePoints : NSSecureCoding {
public static var supportsSecureCoding = true
}
#objc(ScribblePointsValueTransformer)
final class ScribblePointsValueTransformer: ValueTransformer {
static let name = NSValueTransformerName(rawValue: String(describing: ScribblePointsValueTransformer.self))
public static func register() {
let transformer = ScribblePointsValueTransformer()
ValueTransformer.setValueTransformer(transformer, forName: name)
}
override class func transformedValueClass() -> AnyClass {
return ScribblePoints.self
}
override class func allowsReverseTransformation() -> Bool {
return true
}
override func reverseTransformedValue(_ value: Any?) -> Any? {
if let data = value as? Data {
do {
if #available(iOS 11.0, *) {
let unarchiver = try NSKeyedUnarchiver(forReadingFrom: data)
unarchiver.requiresSecureCoding = false
let decodeResult = unarchiver.decodeObject(of: [NSArray.self, ScribblePoints.self], forKey: NSKeyedArchiveRootObjectKey)
if let points = decodeResult as? [CGPoint] {
return points
}
} else {
// Fallback on earlier versions
if let data = value as? Data {
if let points = NSKeyedUnarchiver.unarchiveObject(with: data) as? [CGPoint] {
return points
}
}
}
} catch {
}
}
return nil
}
override func transformedValue(_ value: Any?) -> Any? {
if let points = value as? [CGPoint] {
do {
if #available(iOS 11.0, *) {
let data = try NSKeyedArchiver.archivedData(withRootObject: points, requiringSecureCoding: true)
return data
} else {
// Fallback on earlier versions
let data = NSKeyedArchiver.archivedData(withRootObject: points)
return data
}
} catch {
}
}
return nil
}
}
In CoreData, the way things are defined is shown below.

Resources