First time SignIn pop-up with Firebase SwiftUI - ios

I'm trying to make a feature to show a pop-up when a user logs in for the first time with Firebase, except that when I print it doesn't work. When printing it returns false in all cases
Good to know, the accounts are created from the console!
AuthView.swift
func on_connect() {
if self.email != "" && self.pass != "" {
Auth.auth().signIn(withEmail: self.email, password: self.pass) { (res, err) in
if err != nil {
self.error = err!.localizedDescription
self.alert.toggle()
return
}
if res?.additionalUserInfo?.isNewUser == true{
print("IM NEW!")
UserDefaults.standard.set(true, forKey: "status")
NotificationCenter.default.post(name: NSNotification.Name("status"), object: nil)
}
else{
UserDefaults.standard.set(true, forKey: "status")
NotificationCenter.default.post(name: NSNotification.Name("status"), object: nil)
}
}
}
}
Thanks for your precious help!

Issue #1:
You have two delivViewModels:
#ObservedObject private var viewModel = delivViewModel()
#ObservedObject var first_log: delivViewModel
The first one is getting created inside LoggedView and the second looks like it's getting passed in via a parameter. It's hard to imagine a scenario where you'd need both, so get rid of one of them so that you don't end up calling functions on both, which happens later on:
self.viewModel.is_first_loggin() // called on viewModel
print("first sign in-> \($first_log.first_log)") // called on first_log
Issue #2:
Auth.auth().signIn, like nearly every network function, is asynchronous. This means that it doesn't return immediately (it'll return at an indeterminate point in the future), so code run directly after it will complete before signIn itself has completed. However, the code in the callback function/closure (what you see in the {} after signIn) will get called after its completion.
In terms of printing the value, I'd just move that code inside of the callback function. You'd have:
Auth.auth().signIn(with: credential) { (result, error) in
if error == nil {
if result?.additionalUserInfo!.isNewUser == true {
self.first_log = true
} else {
self.first_log = false
}
print("First log in? \(self.first_log)")
}
}
In terms of showing the popup that you mentioned, you can show that conditionally based on $viewModel.first_log (this is assuming that you've chosen viewModel to be the delivViewModel you want to keep.
Something like:
.sheet(isPresented: $viewModel.first_log) {
Text("Popup stuff")
}
If you did want a way to respond to first_log in your view once it is asynchronously set, you could also look into onReceive (How can I get data from ObservedObject with onReceive in SwiftUI?), however there may not be a use case for it here (unless you're going to drop back into UIKit to display the popup imperatively or something).

Related

Is it possible to ask for the user's PIN, Face ID or Touch ID before he sees a UIView?

I'd like to have the history section of an app locked for everyone besides the person who owns the phone. I don't like forcing the user to make an account or make a new PIN just for this app. Can I authorize using the PIN, Face ID or Touch ID he has already set up?
The Local Authentication framework will handle this.
Here is part of an Apple code sample:
/// Logs out or attempts to log in when the user taps the button.
#IBAction func tapButton(_ sender: UIButton) {
if state == .loggedin {
// Log out immediately.
state = .loggedout
} else {
// Get a fresh context for each login. If you use the same context on multiple attempts
// (by commenting out the next line), then a previously successful authentication
// causes the next policy evaluation to succeed without testing biometry again.
// That's usually not what you want.
context = LAContext()
context.localizedCancelTitle = "Enter Username/Password"
// First check if we have the needed hardware support.
var error: NSError?
if context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) {
let reason = "Log in to your account"
context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason ) { success, error in
if success {
// Move to the main thread because a state update triggers UI changes.
DispatchQueue.main.async { [unowned self] in
self.state = .loggedin
}
} else {
print(error?.localizedDescription ?? "Failed to authenticate")
// Fall back to a asking for username and password.
// ...
}
}
} else {
print(error?.localizedDescription ?? "Can't evaluate policy")
// Fall back to a asking for username and password.
// ...
}
}
}

FaceID/TouchID success case keeps prompting for further authentication

I've implemented password/TouchID/FaceID on a view controller and when I hit the success case, I'd expect the prompt to stop firing but it just fires over and over again.
In my VC:
var context: LAContext!
func authenticateReturningUser() {
context = LAContext()
var error: NSError?
if context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) {
let reason = "Verify that this is your device to continue."
context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason) { success, error in
DispatchQueue.main.sync {
guard success else {
guard let error = error else {
// show error
return
}
switch error {
case LAError.userCancel:
// do stuff
return
default: return
}
}
print("success")
}
}
}
}
The prompt should fire once and not again if the user successfully authorizes
Edit:
authenticateReturningUser is called from the AppDelegate's applicationDidBecomeActive function:
self.coverVC?.completionHandler = { self.removeBackgroundVC() }
self.coverVC?.authenticateReturningUser()
As far as I remember, when showing the Touch ID prompt, your app becomes inactive. So when the prompt is dismissed, your app becomes active again, triggering the App Delegate's applicationDidBecomeActive again.
You might consider introducing a flag that stores whether the app became inactive because of Touch ID / Face ID etc. or because of another reason and use it in applicationDidBecomeActive to decide if authentication should be triggered or not.
Where are you calling authenticateReturningUser()? You may want to create a static boolean authenticated that if false, allows the call to authenticateReturningUser(), and if true, skips the call, and set authenticated = true after calling the function once.

Layer synchronizeWithRemoteNotification not getting called in iOS

I'm building an iOS app and making use of layer.
According to the documentation, when a push notification for a message comes in, you should not immediately switch to the conversation because the message might not have finished synching. So you call a synchronise method and pass it a completion function as follows:
self.layerClient!.synchronizeWithRemoteNotification(userInfo, completion: { (conversation, message, error) in
if (conversation != nil || message != nil) {
NSNotificationCenter.defaultCenter().postNotificationName(CXOConstants.Notifications.BadgeChatTabNotification.rawValue, object: nil, userInfo: userInfo)
let msgPart = message?.parts.first
if msgPart?.MIMEType == "text/plain"{
let msgText = String(data: msgPart!.data!,encoding: NSUTF8StringEncoding)
debugPrint("msgText:: \(msgText)")
} else {
debugPrint("The user sent an image")
}
completionHandler(.NewData)
} else {
completionHandler(.Failed)
}
})
Problem is, this method never gets called. I have tried implementing the same logic outside of the synchronizeWithRemoteNotification method, but when I switch to the conversation, it takes a couple seconds for the message to appear in the conversation.
Could use some help here figuring out why the method isn't getting called.
Thanks :)

Firebase Connection manager should return only one result

I am following documentation located at: https://www.firebase.com/docs/ios/guide/offline-capabilities.html#section-connection-state
However, my implementation and test of:
connectionCheck.observeEventType(.Value, withBlock: { snapshot in
let connected = snapshot.value as? Bool
if connected != nil && connected! {
print("Connected")
} else {
print("Not connected")
}
})
The output in Xcode notes:
Not connected
Connected
If however I turn off the wifi, the result is simply:
Not connected
Given I wish to allow actions to occur and present to the user if there is not a connection, how can I ensure that this Firebase listener only returns the correct response once?
As a suggestion, you may want to add some additional functionality;
If you want to know about the users connection to Firebase, you should observe the .info/connected path, as stated in the docs.
The design pattern I use is to set up a global app variable called isConnected, and attach a Firebase observer to the .info/connected path.
Then in my app wherever I need to know if the user is connected or not, I attach a KVO Observer to my global isConnected var.
When the user disconnects (or connects), the Firebase observer is called which sets isConnected = false (or true).
Then, any places in my app that are observing the isConnected var is notified of the disconnect/connect and can take appropriate action.
Here's the code
let connectedRef = rootRef.childByAppendingPath(".info/connected")
connectedRef.observeEventType(.Value, withBlock: { snapshot in
self.isConnected = snapshot.value as! Bool //KVO property
//this is just so you can see it's being set, remove
if ( self.isConnected == true ) {
print("connected")
} else {
print("not connected")
}
// testing code
})

Setting a subview.hidden = false locks up UI for many seconds

I'm using a button to populate a UIPickerView on a hidden UIVisualEffectView. The user clicks the button, the VisualEffectView blurs everything else, and the PickerView displays all the names in their contact list (I'm using SwiftAddressBook to do this.)
This works fine except when the user clicks the button, the UI locks up for about 5-10 seconds. I can't find any evidence of heavy CPU or memory usage. If I just print the sorted array to the console, it happens almost immediately. So something about showing the window is causing this bug.
#IBAction func getBffContacts(sender: AnyObject) {
swiftAddressBook?.requestAccessWithCompletion({ (success, error) -> Void in
if success {
if let people = swiftAddressBook?.allPeople {
self.pickerDataSource = [String]()
for person in people {
if (person.firstName != nil && person.lastName != nil) {
//println("\(person.firstName!) \(person.lastName!)")
self.pickerDataSource.append(person.firstName!)
}
}
//println(self.pickerDataSource)
println("done")
self.sortedNames = self.pickerDataSource.sorted { $0.localizedCaseInsensitiveCompare($1) == NSComparisonResult.OrderedAscending }
self.pickerView.reloadAllComponents()
self.blurView.hidden = false
}
}
else {
//no success, access denied. Optionally evaluate error
}
})
}
You have a threading issue. Read. The. Docs!
requestAccessWithCompletion is merely a wrapper for ABAddressBookRequestAccessWithCompletion. And what do we find there?
The completion handler is called on an arbitrary queue
So your code is running in the background. And you must never, never, never attempt to interact with the user interface on a background thread. All of your code is wrong. You need to step out to the main thread immediately at the start of the completion handler. If you don't, disaster awaits.

Resources