I'm comming accross an issue similar to this one. Basically every time my app starts, I have to login with my Google Account.
Then, I have this property:
var isGoogleSessionOpen: Bool {
return GIDSignIn.sharedInstance().currentUser != nil
}
which is called as soon as the app starts to check if I have to show the LoginViewController or not.
My problem is that this call is always nil in first place, so I have to login every time my app starts.
also, as it's mentioned here, I'm configuring the scope like this:
if let signIn = GIDSignIn.sharedInstance() {
signIn.scopes = ["https://www.googleapis.com/auth/plus.login","https://www.googleapis.com/auth/plus.me"]
}
Any idea pls?
Regards
You need to sign in when the app starts.
GIDSignIn.sharedInstance().signInSilently()
Related
I am using CloudKit to store publicly available data and the new NSPersistentCloudKitContainer as part of my Core Data stack to store/sync private data.
When a user opens my app, they are in 1 of 4 states:
They are a new user with access to iCloud
They are a returning user with access to iCloud
They are a new user but do not have access to iCloud for some reason
They are a returning user but do not have access to iCloud for some reason
States 1 and 2 represent my happy paths. If they are a new user, I'd like to seed the user's private store with some data before showing the initial view. If they are a returning user, I'd like to fetch data from Core Data to pass to the initial view.
Determining new/old user:
My plan is to use NSUbiquitousKeyValueStore. My concern with this is handling the case where they:
download the app -> are recorded as having launched the app before -> delete and reinstall/install the app on a new device
I assume NSUbiquitousKeyValueStore will take some time to receive updates so I need to wait until it has finished synchronizing before moving onto the initial view. Then there's the question of what happens if they don't have access to iCloud? How can NSUbiquitousKeyValueStore tell me if they are a returning user if it can't receive the updates?
Determining iCloud access:
Based on the research I've done, I can check if FileManager.default.ubiquityIdentityToken is nil to see if iCloud is available, but this will not tell me why. I would have to use CKContainer.default().accountStatus to learn why iCloud is not available. The issue is that is an asynchronous call and my app would have moved on before learning what their account status is.
I'm really scratching my head on this one. What is the best way to gracefully make sure all of these states are handled?
There's no "correct" answer here, but I don't see NSUbiquitiousKeyValueStore being a win in any way - like you said if they're not logged into iCloud or don't have network access it's not going to work for them anyway. I've got some sharing related stuff done using NSUbiquitiousKeyValueStore currently and wouldn't do it that way next time. I'm really hoping NSPersistentCloudKitContainer supports sharing in iOS 14 and I can just wipe out most of my CloudKit code in one fell swoop.
If your app isn't functional without cloud access then you can probably just put up a screen saying that, although in general that's not a very satisfying user experience. The way I do it is to think of the iCloud sync as truly asynchronous (which it is). So I allow the user to start using the app. Then you can make your call to accountStatus to see if it's available in the background. If it is, start a sync, if it's not, then wait until it is and then start the process.
So the user can use the app indefinitely standalone on the device, and at such time as they connect to the internet everything they've done on any other device gets merged into what they've done on this new device.
I struggled with this problem as well just recently. The solution I came up with was to query iCloud directly with CloudKit and see if it has been initialized. It's actually very simple:
public func checkRemoteData(completion: #escaping (Bool) -> ()) {
let db = CKContainer.default().privateCloudDatabase
let predicate = NSPredicate(format: "CD_entityName = 'Root'")
let query = CKQuery(recordType: .init("CD_Container"), predicate: predicate)
db.perform(query, inZoneWith: nil) { result, error in
if error == nil {
if let records = result, !records.isEmpty {
completion(true)
} else {
completion(false)
}
} else {
print(error as Any)
completion(false)
}
}
}
This code illustrates a more complex case, where you have instances of a Container entity with a derived model, in this case called Root. I had something similar, and could use the existence of a root as proof that the data had been set up.
See here for first hand documentation on how Core Data information is brought over to iCloud: https://developer.apple.com/documentation/coredata/mirroring_a_core_data_store_with_cloudkit/reading_cloudkit_records_for_core_data
to improve whistler's solution on point 3 and 4,
They are a new user but do not have access to iCloud for some reason
They are a returning user but do not have access to iCloud for some reason
one should use UserDefaults as well, so that it covers offline users and to have better performance by skipping network connections when not needed, which is every time after the first time.
solution
func isFirstTimeUser() async -> Bool {
if UserDefaults.shared.bool(forKey: "hasSeenTutorial") { return false }
let db = CKContainer.default().privateCloudDatabase
let predicate = NSPredicate(format: "CD_entityName = 'Item'")
let query = CKQuery(recordType: "CD_Container", predicate: predicate)
do {
let items = (try await db.records(matching: query)).matchResults
return items.isEmpty
} catch {
return false
// this is for the answer's simplicity,
// but obviously you should handle errors accordingly.
}
}
func showTutorial() {
print("showing tutorial")
UserDefaults.shared.set(true, forKey: "hasSeenTutorial")
}
As it shows, after the first time user task showTutorial(), UserDefaults's bool value for key "hasSeenTutorial" is set to true, so no more calling expensive CK... after.
usage
if await isFirstTimeUser() {
showTutorial()
}
I have a universal link, say: https://universallink.com/account/settings
When my application is fully closed, this works perfectly. However, if app is open in background and the link is clicked from safari or from slack, or something like that, this is non functional.
What I have observed:
application(\_:willContinueUserActivityWithType:) is of type "NSUserActivityTypeBrowsingWeb"
application.userActivity is nil OR application.userActivity not nil but application.userActivity.webpageURL is nil during application(\_:willContinueUserActivityWithType:)
application(\_:continue:restorationHandler:) is never called.
application(_:didFinishLaunchingWithOptions:) and application(_:willFinishLaunchingWithOptions:) are returning true.
Checking for let url = launchOptions?[.url] as? URL in application(_:didFinishLaunchingWithOptions:) and application(_:willFinishLaunchingWithOptions:) finds nothing.
I am not exactly sure how to proceed in fixing this scenario. The user activity clearly knows that is was opened with a link, but the link is never preserved so as to be handled.
Does anyone know something else that may be causing this issue?
My app won't log out the current user after the app has been uninstalled from the phone. I don't want the user to uninstall the app, and when they reinstall it they are already logged in.
I think it has something to do with its keychain access maybe? Not sure. I was thinking maybe I needed to just un-authenticate the user once the app was deleted, but there is no way of checking that condition. The closest thing to that would be running the applicationWillTerminate function, but if I put my FIRAuth.auth()?.signOut() in there, it would sign my user out every time the app was killed. I don't want that.
How would I go about making this work?
While there is no function or handler for checking when the app has been uninstalled from the phone, we can check if it is the apps first launch. More than likely when an app is first launched, that also means it has just been installed and nothing has been configured within the app. This process will be executed in didfinishLaunchingWithOptions above the return true line.
First, we have to set up the User Defaults:
let userDefaults = UserDefaults.standard
After this, we need to check if the app has launched before or has been run before:
if (!userDefaults.bool(forKey: "hasRunBefore")) {
print("The app is launching for the first time. Setting UserDefaults...")
// Update the flag indicator
userDefaults.set(true, forKey: "hasRunBefore")
userDefaults.synchronize() // This forces the app to update userDefaults
// Run code here for the first launch
} else {
print("The app has been launched before. Loading UserDefaults...")
// Run code here for every other launch but the first
}
We have now checked if it is the apps first launch or not. Now we can try to log out our user. Here is how the updated conditional should look:
if (!userDefaults.bool(forKey: "hasRunBefore")) {
print("The app is launching for the first time. Setting UserDefaults...")
do {
try FIRAuth.auth()?.signOut()
} catch {
}
// Update the flag indicator
userDefaults.set(true, forKey: "hasRunBefore")
userDefaults.synchronize() // This forces the app to update userDefaults
// Run code here for the first launch
} else {
print("The app has been launched before. Loading UserDefaults...")
// Run code here for every other launch but the first
}
We have now checked if the user is launching the app for the first time, and if so, log out a user if one was previously signed in. All the code put together should look like the following:
let userDefaults = UserDefaults.standard
if (!userDefaults.bool(forKey: "hasRunBefore")) {
print("The app is launching for the first time. Setting UserDefaults...")
do {
try FIRAuth.auth()?.signOut()
} catch {
}
// Update the flag indicator
userDefaults.set(true, forKey: "hasRunBefore")
userDefaults.synchronize() // This forces the app to update userDefaults
// Run code here for the first launch
} else {
print("The app has been launched before. Loading UserDefaults...")
// Run code here for every other launch but the first
}
I wanted to set up a conditional segue (in Swift/Xcode, with parse-server framework, hosted by Heroku) that will perform only if the current user has not logged out. It works appropriately if the user has not logged out. However, even if the user logs out, the segue will still run. Here is a basic set up of the code.
I call the user to log out via:
PFUser.logoutinbackground()
However, the segue:
If PFUser.currentuser() != nil {
//perform segue
}
will still perform the segue. Printing the PFUser.currentuser() to logs does not return nil. If I rewrite the conditional segue code to this:
If PFUser.currentuser()?.username != nil {
//perform segue
}
This works appropriately, returning PFUse.currentuser()?.username to the logs does return nil. Is there anything particularly wrong with leaving it like this (ie. will this cause any problems?). Also, why wouldn't it be return nil for the first version of the segue code? Does it have to do with the fact that I called PFUser.loginoutinbackground() as opposed to PFUser.logout() (the latter causes the app to crash for some reason)?
PFUser.logOut(); This logs the user out immediately. When you call PFUser.logoutinbackground() does not log the user out immediately.
PFUser.logoutinbackground()
if PFUser.currentuser() != nil {
//perform segue
}
logoutinbackground() is async. That is why the code above will not do what you think it should do. I always use PFUser.logOut();
Update
Make sure that you do not have this in your code
PFUser.enableAutomaticUser()
I am working on a new version of my app. I was using the sandbox normally for a while, but now all of my devices are stuck with a very strange problem. They appear to be halfway logged into game center. It doesn't work for them, but they can't log out either. Here is my authentication method:
- (void)authenticateLocalPlayer {
GKLocalPlayer* localPlayer = WJLocalPlayer;
WJLog(#"Authenticating local user...");
if (localPlayer.authenticated == NO) {
localPlayer.authenticateHandler = ^ (UIViewController* vc, NSError *error) {
if (error) {
WJLog(#"Authentication failed! %#", [error localizedDescription]);
}
else {
WJLog(#"Authentication succeeded!");
NSString* name = [GKLocalPlayer localPlayer].displayName;
WJLog(#"display name is %#", name);
NSString* alias = [GKLocalPlayer localPlayer].alias;
WJLog(#"alias is %#", alias);
GKTurnBasedEventHandler *ev = [GKTurnBasedEventHandler sharedTurnBasedEventHandler];
ev.delegate = self;
}
};
}
}
And here is what I am seeing from the log statements [WJLog is just my own version of NSLog without the garbage]:
Authenticating local user...
Authentication succeeded!
display name is Me
alias is (null)
I can log in or out in the game center app. It makes no difference. I always see the above. I even tried restoring one of the devices to factory settings. The result was still the same. I also tried disabling and re-enabling game center for the new version of my app. Still the same result.
Any ideas?
You're completely ignoring the UIViewController parameter. You're supposed to present this to the user if it exists, so they can log in. Probably you are only now experiencing this because you logged in to the non-sandbox game center, and now when you run the app it wants to ask you for your sandbox credentials, but instead you're assuming you're authenticated.
You have some other problems here too:
You should set the authenticateHandler once only, soon after your app launches.
You should check localPlayer.authenticated inside your authenticateHandler, and nowhere else, as this is the only place it's guaranteed to be valid. Specifically, it's a meaningless value after you resume from the background and until your authenticateHandler gets called again. If you need it elsewhere, use a global variable that gets initialised to false at startup and also in your applicationWillEnterForeground method, and only gets set to true inside your authenticateHandler when you've determined that the localPlayer is actually authenticated.
Check the error and log it by all means, but it doesn't tell you anything about whether authentication actually succeeded, so remove the 'else'.
Have a look at the documentation here.