Start a new topic
Answered

iOS SDK 3.0 Bug - custom class for User isn't instantiated

I am using iOS sdk 3.0. I have created a custom user sublclass as explained in http://devcenter.kinvey.com/ios-v3.0/guides/users#CustomAttributesCustomUserObject


below is my SDK initializatin code in appDelegate:


aKinvey.sharedClient.userType = CustomKinveyUser.self

       Kinvey.sharedClient.initialize(

            appKey: "kid_xxxxx",

            appSecret: "my-app-secret"

        )


The CustomKinveyUser class looks like:


class CustomKinveyUser: Kinvey.User {

    var profilePicURL: String?

    override func mapping(map: Map) {

        super.mapping(map: map)

        profilePicURL <- map["profilePicURL"]

    }

}


At some point in the app, after I have a logged-in session,  I am doing this - 

 guard let user = Client.sharedClient.activeUser as? CustomKinveyUser else {return}

 

The above line works and user is non-nil and is of type CustomKinveyUser

if and only if I have signed in during that particular launch of the app.


However, in subsequent app launches if I  already have a signed-in session, Client.sharedClient.activeUser returns a non-nil object as expected, but the class type is the base user class (Kinvey.User) instead of CustomKinveyUser and so the downcast fails (i.e the as? CustomKinveyUser part returns nil) 


captured from the xcode console:


(lldb) po Client.sharedClient.activeUser

▿ Optional<User>

  ▿ some : <__KNVUser: 0x7fdf34e829d0>




Best Answer
Niraj,

Thanks for the detailed explanations. Engineering is looking at the issue.

We'd like you to know that our SDK is open source and we are happy to accept contributions. If you have a solution for this issue or any others in the future, please feel free to submit a pull request to our repo. Our team will gladly review it and accept your fix.

 

Thanks,

Pranav

Kinvey


Niraj,

I tried creating a CustomUser class and had an address field added to it.

 

class CustomUser: User {
    var address: String?
    override func mapping(map: ObjectMapper.Map) {
        super.mapping(map: map)
        address <- map["address"]
    }
}

 


Just tried the below code to reproduce the issue, but everytime I see the address being printed properly after login. I am running it on the simulator.

 

 

User.login(username: "mind1", password: "mind1") { user, error in
            if let user = user as? CustomUser {
                //the log-in was successful and the user is now the active user and credentials saved
                //hide log-in view and show main app content
                print("User: \(user)")
                print(user.address)
                guard let user1 = Client.sharedClient.activeUser as? CustomUser else {return}
                print(user1.address)

 

 

Is there anything specific that you are doing? Are you able to reproduce it consistently?


Thanks,
Pranav
Kinvey

 

Thanks for looking into it, Pranav.


The above code works because you are checking for the type returned by Client.sharedClient.activeUser right after logging in. It will return CustomUser if you call it any time in the future unless you kill the app.


What I am doing on every cold launch of the app is I am checking the value returned by Client.sharedClient.activeUser and only if activeUser is nil, I am showing the login screen and calling User.login().

This is a very common use case ( require users to login only if can't find saved credentials)

To reproduce the problem I am seeing, you'll have to check for activeUser before making your  login call ( and skip the login call if activeUser is not nil) :

 

if let activeUser = Client.sharedClient.activeUser {
  if let customUser = activeUser as? CustomUser {
      print("Yeah! We have an activeUser:  \(activeUser) and it is of type CustomUser. We can skip the call to Login")
   } else {
      fatalError("We have an activeUser: \(activeUser) but it's not of type CustomUser")
   }
} else  {
   User.login(username: "mind1", password: "mind1") { user, error in
            if let user = user as? CustomUser {
                //the log-in was successful and the user is now the active user and credentials saved
                //hide log-in view and show main app content
                print("User: \(user)")
                print(user.address)
                guard let user1 = Client.sharedClient.activeUser as? CustomUser else {return}
                print(user1.address)
}

 

If you do a clean install of you app with the above code, then it will call User.Login in the first run because activeUser will be nil.

 However, if you kill the app and re-run it, you'd expect it to print "Yeah! We have an activeUser: ....", but instead it will go to the else part to abort your app with the fatalError.


I dived a bit into Kinvey SDK's implementation of custom User class and it seems this problem has to do with the difference between how the User object is retrieved from its in-memory version VS its archived version (version that is persisted and un-archived on app launch). After a successful User.login() call, the SDK respects the type set in sharedClient.userType and instantiates the correct sublcass to assign to activeUser. This is somehow not true when after you initialize the SDK on cold app launch. In that case, sharedClient.userType is not respected.


I hope this explains the problem.

Niraj,


The active user object can be refreshed from the server using the User.get(client.activeUser)method. (http://devcenter.kinvey.com/ios/guides/users#usermanagement)

 

Can you try calling the User.get() using the Kinvey.sharedClient.activeUser.userId? This should get back the CustomUser.


Something like this:

 

User.get(userId: (Kinvey.sharedClient.activeUser?.userId)!, client: Kinvey.sharedClient, completionHandler: {activeUser, error in

 

            if let customUser = activeUser as? CustomUser {

                print("Yeah! We have an activeUser:  \(activeUser) and it is of type CustomUser. We can skip the call to Login")

            } else {

                fatalError("We have an activeUser: \(activeUser) but it's not of type CustomUser")

            }

            

        })

 



Let me know if this helps.


Thanks,

Pranav

Kinvey

Thanks again for working on it.


I am pretty sure calling User.get( ) would work, but it's a workaround at best which isn't ideal.


Consider the practical use case - If users have logged in previously, I would like to give them the logged-in experience immediately on subsequent app launches instead of waiting for a network call to complete


IMO the class mismatch is clearly a bug. I request to forward it to the appropriate team/dev.

I would be glad to come up with a fix too!



Niraj,

Thanks for the feedback. I will forward it to engineering team and update you once I have more information from them.

Thanks,
Pranav
Kinvey Support

 

Niraj,

This has been escalated to engineering and will update you once I have more information from them.

Thanks,
Pranav
Kinvey Support
MLIBZ-1621

 

Hi Pravav,


If it helps, here is a fix:

The culprit is this line in initialize(  with all params ) func  in Client.swift

let user = Mapper<User>().map(JSON: json) 


 It ignores the `userType` while unarchiving the saved user and always instantiates the saved JSON using Kinvey.User class.

Replacing it by what is done in the API-response parsing flow in (JsonResponseParser's parseUser function) fixes the problem:

           

 let map = Map(mappingType: .fromJSON, JSON: json)
 let user = userType.init(map: map)
user?.mapping(map: map)

 


Answer
Niraj,

Thanks for the detailed explanations. Engineering is looking at the issue.

We'd like you to know that our SDK is open source and we are happy to accept contributions. If you have a solution for this issue or any others in the future, please feel free to submit a pull request to our repo. Our team will gladly review it and accept your fix.

 

Thanks,

Pranav

Kinvey

Any update on this issue?  I am still unable to use CustomUser on the initial viewController if the user is not forced to login every time.  Downcasting the User to a CustomUser only works when logging in.


Thanks,

James

James,

 

Can you try the fix that Niraj has mentioned above?


Thanks,

Pranav

Kinvey

Niraj's fixed worked. Thank you!

Hey Pranav,


I noticed that Niraj's fix is not in the latest version released about a week ago, 3.3.7. Is a fix in the works for that or should one of us create a pull request?

Michael,

We'd like you to know that our SDK is open source and we are happy to accept contributions. If you have a solution for this issue or any others in the future, please feel free to submit a pull request to our repo. Our team will gladly review it and accept your fix.

Thanks,
Pranav
Kinvey

 

Hi All,


I am having the exact same issue, its a nuisance that the user can't be downcasted to a custom user class. at first i thought that the user object wasnt being saved, this is because the downcast failed it would return nil. i later checked the non-downcasted userobject for nil and it was not, it is returning a user but by default Kinvey.User type.

Kinvey is using a 

Login or Signup to post a comment