6

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.

rmaddy
  • 314,917
  • 42
  • 532
  • 579
tonytran
  • 1,068
  • 2
  • 14
  • 24
  • Related: https://stackoverflow.com/questions/33319959/nsobject-subclass-in-swift-hash-vs-hashvalue-isequal-vs# – Martin R Aug 08 '17 at 02:46

2 Answers2

14

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
}
matt
  • 515,959
  • 87
  • 875
  • 1,141
  • thanks. it works. However, i want to fully understand. I know `other` is a new `User`and `self` will be previous `User` which are form the list. My uncertainty is how `self` can be referenced to previous `User` from the list. – tonytran Jul 14 '16 at 21:35
  • Don't worry about that. After all, you didn't worry about it for `==`. All you need to know is that `contains` depends on this. – matt Jul 14 '16 at 22:58
3

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)
}
Luca Angeletti
  • 58,465
  • 13
  • 121
  • 148
  • I also tried and it works as well. I was surprised that `contains` logic is used to compare the memory address. Can you explain this line `$0.login == user.login `.What is `$0` means Further more, it seems to be that I dont need to conform `func ==` in my code any more. – tonytran Jul 14 '16 at 21:40
  • @tonytran: The problem is that you inherit `NSObject` so the `isEqual` method is invoked to check for equality. – Luca Angeletti Jul 14 '16 at 21:42
  • 1
    @tonytran: `{ $0.login == user.login }` here `$0` is the n-th element of `users`. This closure is applied to every element of the array until it is `true`. In this case the `contains` returns `true`. Otherwise `contains` does `false`. – Luca Angeletti Jul 14 '16 at 21:45
  • @tonytran: __YES__ `users.contains { $0 == user }` will work fine. – Luca Angeletti Jul 14 '16 at 22:06
  • :please dont shout :). and how is about === ? – tonytran Jul 14 '16 at 22:16
  • 1
    @tonytran: No problem :D This `===` always compare memory addresses – Luca Angeletti Jul 14 '16 at 22:19