Search code examples
iosswiftrealm

Google signIn connected with Realm Swift


I have an iOS app making a SignIn using google Authentication and save the user inside Realm database for the first time the user object saved alright if I sign out and then sign in with the same user again the app crashed.

The Realm Model

import Foundation
import RealmSwift
import CoreLocation

class UserDB: Object {

  @objc dynamic var id : String!
  @objc dynamic var name: String?
  @objc dynamic var email: String?
  @objc dynamic var phoneNumber: String?
  @objc dynamic var photo: String?
  @objc dynamic var latitute: String?
  @objc dynamic var longitute: String?
  @objc dynamic var supporter: String?


 override static func primaryKey()-> String? {
    return "email"
  }

}

Here SignIn function

func sign(_ signIn: GIDSignIn!, didSignInFor user: GIDGoogleUser!, withError error: Error!) {

    if let error = error {
        if (error as NSError).code == GIDSignInErrorCode.hasNoAuthInKeychain.rawValue {
            print("The user has not signed in before or they have since signed out.")
        } else {
            print("\(error.localizedDescription)")
        }
        return
    }
    // Perform any operations on signed in user here.
     let userId = user.userID                 
     let idToken = user.authentication.idToken 
     let fullName = user.profile.name
     let givenName = user.profile.givenName
     let familyName = user.profile.familyName
     let email = user.profile.email
     let photo = user.profile.imageURL(withDimension: 200)

    //Print all fields
    if let email = email {
        print("Your email is \(email)")
    }
    if let userId = userId {
        print("Your Id is \(userId)")
    }
    if let fullName = fullName {
        print("Your Full name is \(fullName)")
    }
    if let idToken = idToken {
        print("Your token is \(idToken)")
    }
    if let familyName = familyName {
        print("Your family Name is \(familyName)")
    }

    if let givenName = givenName {
        print("Your givin name is \(givenName)")
    }
    if let photo = photo {
        print("Your image is : \(photo)")
    }

    //Realm Object
    account.id = userId
    account.name = givenName
    account.email = email
    if let photo = photo?.absoluteString {
        account.photo = photo
    }

    saveData(data: account)


        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        let vc = storyboard.instantiateViewController(withIdentifier: "GoogleMaps") as! GoogleMaps
        vc.accountMap = account
        navigationController?.pushViewController(vc, animated: true)


    } else {
        let alert = UIAlertController(title: "UnSuccessful SignIn", message: "please try again", preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "Close" , style: .cancel) { (action) in})
        self.present(alert,animated:true,completion:nil)

    }
}

and then call saveData function to save the object into the Realm Database

func saveData(data: UserDB) {
    do {
        try realm.write {
            let user = realm.object(ofType: UserDB.self, forPrimaryKey: data.email!)
            if let user = user {
                print("array count: ",user)
                print("We found ",user.email!)
                realm.add(user, update: .modified)
            } else {
                print("We didn't find ",data.email!)
                //realm.add(data)
                realm.create(UserDB.self, value: data, update: .all)
            }
        }
    } catch {
        print("Error saving account Data \(error)")
    }

    //here put the tableView reloadData()
    //tableView.realoadData()
}

so for the first signIn it's alright and the user info get saved but if I sign out and then signIn again the app crashed.

The Error

2019-12-26 12:05:26.504060+0200 social2[1943:339412] *** Terminating app due to uncaught exception 'RLMException', reason: 'Attempting to modify object outside of a write transaction - call beginWriteTransaction on an RLMRealm instance first.'
*** **First throw call stack:**
(
    0   CoreFoundation                      0x00000001088d68db __exceptionPreprocess + 331
    1   libobjc.A.dylib                     0x000000010aa65ac5 objc_exception_throw + 48
    2   Realm                               0x000000010928e349 _ZL27RLMVerifyInWriteTransactionP13RLMObjectBase + 105
    3   Realm                               0x000000010929399d _ZN12_GLOBAL__N_18setValueEP13RLMObjectBasemP8NSString + 29
    4   Realm                               0x0000000109293971 _ZZZN12_GLOBAL__N_110makeSetterIU8__strongP8NSStringS3_EEP11objc_objectP11RLMPropertyEUb0_ENKUlvE_clEv + 97
    5   Realm                               0x00000001092938d6 ___ZN12_GLOBAL__N_110makeSetterIU8__strongP8NSStringS3_EEP11objc_objectP11RLMProperty_block_invoke_2 + 310
    6   social2                             0x0000000103b8264b $s7social214ViewControllerC4sign_12didSignInFor9withErrorySo07GIDSignG0CSg_So13GIDGoogleUserCSgs0J0_pSgtF + 9851
    7   social2                             0x0000000103b836f3 $s7social214ViewControllerC4sign_12didSignInFor9withErrorySo07GIDSignG0CSg_So13GIDGoogleUserCSgs0J0_pSgtFTo + 147
    8   social2                             0x0000000104123924 __37-[GIDSignIn addCallDelegateCallback:]_block_invoke + 116
    9   social2                             0x000000010411ea46 -[GIDCallbackQueue fire] + 161
    10  social2                             0x000000010411e489 +[GIDAuthentication handleTokenFetchEMMError:completion:] + 364
    11  social2                             0x0000000104122b95 __38-[GIDSignIn maybeFetchToken:fallback:]_block_invoke + 311
    12  AppAuth                             0x0000000104f97060 __86+[OIDAuthorizationService performTokenRequest:originalAuthorizationResponse:callback:]_block_invoke_13 + 48
    13  libdispatch.dylib                   0x0000000108f9dd7f _dispatch_call_block_and_release + 12
    14  libdispatch.dylib                   0x0000000108f9edb5 _dispatch_client_callout + 8
    15  libdispatch.dylib                   0x0000000108fac080 _dispatch_main_queue_callback_4CF + 1540
    16  CoreFoundation                      0x000000010883da79 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 9
    17  CoreFoundation                      0x0000000108838126 __CFRunLoopRun + 2310
    18  CoreFoundation                      0x00000001088374d2 CFRunLoopRunSpecific + 626
    19  GraphicsServices                    0x000000010e1042fe GSEventRunModal + 65
    20  UIKitCore                           0x0000000117cbdfc2 UIApplicationMain + 140
    21  social2                             0x0000000103b89b4b main + 75
    22  libdyld.dylib                       0x000000010c7cd541 start + 1
    23  ???                                 0x0000000000000001 0x0 + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
(lldb) 

Solution

  • I am a bit unclear on the question but I think what is being asked is

    When signing in using Google Sign-In and the user object does not exist in Realm, how do I add it to Realm upon first sign in, and then be able to access it thereafter when the user signs in.

    It appears by the code in the question we are simply duplicating all of the GIDGoogleUser properties in Realm so I will omit some of that to keep it brief.

    Additionally the question is leveraging a users email address as a key and that can be problematic as email addresses change and that could invalidate all data. A safer bet is to leverage the unique user id that Google assigns to each user.

    So, starting with a Realm user object with a primary key of user_id

    class RealmUserClass: Object {
        @objc dynamic var user_id = ""
        @objc dynamic var full_name = ""
        @objc dynamic var email = ""
    
        override static func primaryKey() -> String? {
            return "user_id"
        }
    }
    

    Then the sign in delegate function, I would write it like this

    func sign(_ signIn: GIDSignIn!, didSignInFor user: GIDGoogleUser!, withError error: Error!) {
        if error == nil {
            self.handleSignIn(forGIDUser: user)
        } else {
            print(error.localizedDescription)
            return
        }
    

    So when the user signs in, we call the handleSignIn function and pass it the google user object

    func handleSignIn(forGIDUser: GIDGoogleUser) {
        let gidUserId = forGIDUser.userID
    
        let realm = try! Realm()
        if let user = realm.object(ofType: RealmUserClass.self, forPrimaryKey: gidUserId) {
            print(user) //the user exists in Realm
            //proceed to the rest of the app
        } else { //user did not exist in Realm, create it}
            let userToAdd = RealmUserClass()
            userToAdd.person_id = forGIDUser.userID
            userToAdd.full_name = forGIDUser.profile.name
            userToAdd.email = forGIDUser.profile.email
            try! realm.write {
                realm.add(userToAdd)
            }
            //proceed to the rest of the app
        }
    }
    

    Note that I omitted a lot of error checking for brevity so be sure to add that in.