I’m using Realm Object Server for a simple test project and I’m facing problems synchronizing ROS connection setup and follow up usage of the realm object to access the database.
In viewDidLoad I’m calling connectROS function to initialize realmRos object/connection:
var realmRos: Realm!
override func viewDidLoad() {
connectROS()
if(FBSDKAccessToken.current() != nil){
// logged in
getFBUserData()
}else{
// not logged in
print("didLoad, FB user not logged in")
}
}
func connectROS() {
let username = "realm-admin"
let password = "*********"
SyncUser.logIn(with: .usernamePassword(username: username, password: password, register: false), server: URL(string: "http://146.185.154.***:9080")!)
{ user, error in
print("ROS: checking user credentials")
if let user = user {
print("ROS: user credentials OK")
DispatchQueue.main.async {
// Opening a remote Realm
print("ROS: entering dispatch Q main async")
let realmURL = URL(string: "realm://146.185.154.***:9080/~/***book_test1")!
let config = Realm.Configuration(syncConfiguration: SyncConfiguration(user: user, realmURL: realmURL))
self.realmRos = try! Realm(configuration: config)
// Any changes made to this Realm will be synced across all devices!
}
} else if let error = error {
// handle error
print("ROS: user check FAIL")
fatalError(String(describing: error))
}
}
}
In viewDidLoad function next step is to get FB logged user (in this case I’m using FB authentication). After the logged FB user is fetched, the application perform check is that FB user is new user for my application and my proprietary ROS User’s table.
func checkForExistingProfile(user: User) -> Bool {
var userThatExist: User?
do {
try self.realmRos.write() {
userThatExist = self.realmRos.object(ofType: User.self, forPrimaryKey: user.userName)
}
} catch let error as NSError {
print("ROS is not connected")
print(error.localizedDescription)
}
if userThatExist != nil {
return true
} else {
return false
}
}
At this point checkForExistingProfile usually (not always) crashes at try self.realmRos.write() which happens to be nil.
I think the problem comes from the synchronization between connectROS execution (which is asynchrony) and checkForExistingProfile which execution comes before connectROS completion.
Since you didn't show how checkForExistingProfile() is called after viewDidLoad() this is conjecture, but based on everything else you described it's the likely cause.
What you need to do is not call checkForExistingProfile() until the sync user has logged in and your self.realmRos variable has been initialized. Cocoa Touch does nothing to automatically synchronize code written using an asynchronous pattern (like logIn(), which returns immediately but reports its actual completion state in a callback), so you need to manually ensure that whatever logIn() is supposed to do has been done before you call any additional code that depends on its completion.
Related
This code shows the app's main view controller. We would like increment the currently logged in user's field value by 1. In the code below we are only able to do this by manually pasting "nDcAFLPpRuPXI9AOLkln" which we copied from fire base itself.
How do we automatically refer to the currently logged in user?
snapchot of our firestore data tree
#IBAction func bidButton(_ sender: Any) {
let updateScore = db.collection("users").document("nDcAFLPpRuPXI9AOLkln")
updateScore.updateData([
"leaderboardscore": FieldValue.increment(Int64(1))
])
db.collection("users").document("nDcAFLPpRuPXI9AOLkln")
.addSnapshotListener { documentSnapshot, error in
guard let document = documentSnapshot else {
// there was an error
print("Error fetching document: \(error!)")
return
}
// no data to show
guard let data = document.data() else {
print("Document data was empty.")
return
}
self.leaderBoardScoreLabel.text = String("Current data: \(data)")
//print("Current data: \(data)")
}
If you are using Firebase Authentication, and asking how to get the currently signed in user, you should follow the instructions in the documentation:
The recommended way to get the current user is by setting a listener on the Auth object:
handle = Auth.auth().addStateDidChangeListener { (auth, user) in
// ...
}
By using a listener, you ensure that the Auth object isn't in an
intermediate state—such as initialization—when you get the current
user.
You can also get the currently signed-in user by using the currentUser
property. If a user isn't signed in, currentUser is nil:
if Auth.auth().currentUser != nil {
// User is signed in.
// ...
} else {
// No user is signed in.
// ...
}
I would strongly suggest learning how to use the listener, which will give you a callback every time the user is seen to sign in or out.
Once you have a User object, you can use its uid property to get the string you're looking for:
let uid = user.uid
I have a strange bug that is occurring only on few user iPhones, details below -
The app consumes a universal framework (developed by ourself) to save accessToken and refreshToken after successful login to the Keychain. We are using Locksmith to achieve the functionality - Save, load data and delete when the user is logged out.
Everytime when the app is killed and launched or applicationWillEnterForeground, the tokens are refreshed with the help of a service call and are saved to keychain again. When the refreshToken expires (this token is valid for one month), user is notified that the app has not been used for a long time and he is logged out.
The actual problem is here that for only few users, the refresh mechanism fails even when they are using the app daily (i.e. not before completion of one month of the refreshToken). After verification with backend team, the refresh service is always up so I suspect the Locksmith loadDataForUserAccount but unable to reproduce the issue. Also, may users do NOT face the problem. Everything works normally as expected.
Can someone help me move further how to identify the cause?
Below is the code to refresh the accessToken and refreshToken
** Refresh token call From the App when app enters foreground or killed and launched**
if let mySession = ServiceLayer.sharedInstance.session {
mySession.refresh { result in
switch result {
case .failure(.authenticationFailure):
if isBackgroundFetch {
print("👤⚠️ Session refresh failed, user is now logged out.")
self.myService.logoutCurrentUser()
// Logout Current user
mySession.invalidate()
self.showLoginUI()
}
else {
// user accessToken is invalid but provide access to QR
// on the home screen. disable all other actions except logout button
self.showHomeScreen()
}
default:
mySession.getAccessToken { result in
switch result {
case let .success(value):
print("Access Token from App Delegate \(value)")
myAccessToken = value
case let .failure(error):
print("❌ Failed to fetch AccessToken: \(error)")
}
}
}
}
}
From the framework where the refresh mechanism is implemented
public func refresh(_ completion: #escaping (MyResult<String, MyError>) -> (Void)) {
guard isValid else {
completion(.failure(.invalidSession))
return
}
getRefreshToken { result in
switch result {
case let .success(refreshToken):
// Get new tokens.
ServiceClient.requestJSON(ServiceRequest.refreshToken(refreshToken: refreshToken)) { result in
switch result {
case let .success(dictionary):
var newAccessToken: String?
var newRefreshToken: String?
for (key, value) in dictionary {
if key as! String == "access_token" {
newAccessToken = value as? String
}
if key as! String == "refresh_token" {
newRefreshToken = value as? String
}
}
guard newAccessToken != nil && newRefreshToken != nil else {
completion(.failure(.general))
return
}
print("Renewed session tokens.")
do {
try Locksmith.updateData(data: [MySession.accessTokenKeychainKey: newAccessToken!, MySession.refreshTokenKeychainKey: newRefreshToken!],
forUserAccount: MySession.myKeychainAccount)
}
catch {
completion(.failure(.general))
}
completion(.success(newAccessToken!))
case let .failure(error):
if error == MyError.authenticationFailure {
print(“Session refresh failed due to authentication error; invalidating session.")
self.invalidate()
}
completion(.failure(error))
}
}
case let .failure(error):
completion(.failure(error))
}
}
}
The app is likely being launched in the background while the device is locked (for app refresh or other background mode you've configured). Protected data (including Keychain) is not necessarily available at that time. You can check UIApplication.isProtectedDataAvailable to check if it's available, and you can reduce the protection of the item to kSecAttrAccessibleAfterFirstUnlock in order to have background access more reliably (though not 100% promised, even in that mode). Locksmith calls this AfterFirstUnlock.
In my iOS/Swift/Firebase app, I am trying to access the "isNewUser" parameter after a user successfully signs in via email/password so that I can pop a window to compel them to reset their password from the temporary one initially assigned upon user creation.
Any insights would be appreciated. Thanks.
The .isNewUser Bool is available from the FirebaseAuth AdditionalUserInfo class.
Here is the link. In order to utilize this code, please see a demo sign in function I wrote below.
Auth.auth().signIn(with: credential) { (result, error) in
if let error = error {
print("Error: \(error)");
return;
}
// Fetch the user's info
guard let uid = result?.user.uid else {return}
// Safely unwrap the boolean value rather than forcing it with "!" which could crash your app if a nil value is found
guard let newUserStatus = result?.additionalUserInfo?.isNewUser else {return}
// Test the value
print("\nIs new user? \(newUserStatus)\n")
if newUserStatus == true {
// Provide your alert prompt
}
else{
// Transition view to continue into the app
}
}
I have an issue when the Realm Object Server have a different IP. The application can login through by Credential but after that it will return empty data although my database sit right on that IP and can be accessed by Realm Browser. Actually, I only use one account in realm object server and I create a user table with username and password so that after it can connect through Credential to the server, I will read the username and password on screen and check it information in database.
Connect to Realm Object Server function:
class func login(username: String, password: String, action: AuthenticationActions, completionHandler: #escaping ()->()) {
let serverURL = NSURL(string: realmIP)!
let credential = Credential.usernamePassword(username: username, password: password, actions: [action])
SyncUser.authenticate(with: credential, server: serverURL as URL) { user, error in
if let user = user {
syncUser = user
let syncServerURL = URL(string: realmURL)!
let config = Realm.Configuration(syncConfiguration: (user, syncServerURL))
realm = try! Realm(configuration: config)
} else if error != nil {
}
completionHandler()
}
}
Query from table after login by SyncUser:
class func loginLocal(employee: String) -> Bool{
let predicate = NSPredicate(format: "employee = %#", employee)
if (realm != nil) {
let user = realm?.objects(MyUser.self).filter(predicate)
if ((user?.count)! > 0) {
return true
}
}
return false
}
The solution seems to be weird so that I have to call a function multiple times by pressing my login button and hope that it will go through to the server.
This is my first application using Realm and Realm Object Server so I don't have much experience in this situation.
I may need more information on how you're handling the logged in Realm after the login, but from the code you've shown there, it looks like you're accidentally accessing a local version of the Realm and not the synchronized one.
Once logged in, you need to make sure you use the same Configuration object whenever you create Realm instances after that. It's not recommended to create and then save the realm instance inside the login completion block, as this block occurs on a background thread, making it unavailable anywhere else.
If your app is always online, it's easier to simply set the sync configuration as the default Realm for your app:
SyncUser.authenticate(with: credential, server: serverURL as URL) { user, error in
if let user = user {
syncUser = user
let syncServerURL = URL(string: realmURL)!
let config = Realm.Configuration(syncConfiguration: (user, syncServerURL))
Realm.Configuration.defaultConfiguration = config
}
completionHandler()
}
Otherwise, you can either save the Configuration in some sort of global object, or recreate it each time you need to create a Realm instance. The important thing to remember is you need to make sure your Realm instance is using a Configuration object with the successfully logged in user, otherwise it will default back to using a normal, empty local Realm.
I try to get userRecordID in airplane mode, but I get an error, any other way?
class func asdf() {
var defaultContainer = CKContainer.defaultContainer()
var publicDatabase = defaultContainer.publicCloudDatabase
defaultContainer.fetchUserRecordIDWithCompletionHandler({ userRecordID, error in
if error == nil {
println("userRecordID.recordName : \(userRecordID.recordName)")
} else {
println("\(error.localizedDescription)")
}
})
}
Terminal: Couldn't renew our secure session
I put an accountStatusWithCompletionHandler call outside of fetchUserRecordIDWithCompletionHandler, that returned CKAccountStatus.Available.
You cannot detect internet connectivity with CloudKit. It will only give you an error when there is no connectivity. If you do want to test for internet connectivity, then you could use the famous Reachability class like this: How to check for an active Internet connection on iOS or OSX?
If you want to detect changes to the iCloud account, then you can add the following code to your AppDelegate application didFinishLaunchingWithOptions:
var localeChangeObserver = NSNotificationCenter.defaultCenter().addObserverForName(NSUbiquityIdentityDidChangeNotification, object: nil, queue: NSOperationQueue.mainQueue()) { _ in
println("The user’s iCloud login changed: should refresh all user data.")
}
If you then want to fetch the user id, you have to do a container.requestApplicationPermission to see if you are allowed to query an then a container.fetchUserRecordIDWithCompletionHandler. Bit this requires internet connection. You could cache it on the device together with the detection code above to get the correct status.
I came across to this code, comparing recently and previous logged in user's token, and if the same, use the previously downloaded userRecordID. The only problem that in some cases on my iPad ubiquityIdentityToken method returns nil even dow I am logged in, strange.
class func checkUser() {
let ubiquityIdentityToken = NSFileManager.defaultManager().ubiquityIdentityToken
let status = Utility.status()
let prevUbiquityIdentityToken = status.objectForKey("ubiquityIdentityToken")
if ubiquityIdentityToken != nil && ubiquityIdentityToken!.isEqual(prevUbiquityIdentityToken) {
} else if ubiquityIdentityToken != nil && !ubiquityIdentityToken!.isEqual(prevUbiquityIdentityToken) {
status.setObject(ubiquityIdentityToken!, forKey: "ubiquityIdentityToken")
Utility.saveStatus(status)
let defaultContainer = CKContainer.defaultContainer()
let publicDatabase = defaultContainer.publicCloudDatabase
defaultContainer.fetchUserRecordIDWithCompletionHandler({ userRecordID, error in
if error == nil {
//do some stuff
})
} else {
println("\(error.localizedDescription)")
}
})
} else {
//do some stuff
status.removeObjectForKey("ubiquityIdentityToken")
Utility.saveStatus(status)
}
}