Understanding addStateDidChangeListener firebase authentication - swift

Hi I am new to firebase and want to confirm my understanding of an auth function.(addStateDidChangeListener)
A little background:
When the app is first launched, the app delegate is called first and then this sceneDelegate function is called. Now it will present a screen based on whether the user is signed in or not. If they are not signed up it will present them with my signin/signup screen (self.createHandleSignInOrSignUp()).
Now 2 questions:
After they sign up or sign in the app jumps back to this block of code inside .addStateDidChangeListener.
This is because the auth state changes, right?
Also while the app is running you can sign out, and then this block of code is called.
This is because this function is always 'listening' for auth state changes while the app is running?
The code below is my part of my SceneDelegate.swift file and the function I have questions about.
Thank you
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
window = UIWindow(frame: windowScene.coordinateSpace.bounds)
window?.windowScene = windowScene
handle = Auth.auth().addStateDidChangeListener({ auth, user in
if (user != nil) {
self.window!.rootViewController = self.createTabBar()
}
else {
self.window!.rootViewController = self.createHandleSignInOrSignUp()
}
})
window?.makeKeyAndVisible()
}

The completion handler for your auth state change listener will be called each time the auth state changes. This typically happens asynchronously, for example when the user signs in or signs out.
The auth state listener remains active until you remove it, or until the app exits.

Related

Change a scene on app becoming active in Swift

I am working to ensure that the user doesn't change their time and currently I test it when the app is starting and if their time is incorrect I perform a segue to a new screen and create an alert to make them change it. However if the user changes their time while the app is open the check won't run. I think it is something with the SceneDelegate as that will run code when the app becomes active but I don't know how to transition it as the code that checks the time and performs the segue is located in ViewController. Using UI kit and webkit.
These are the functions responsable for calling the time error message. Both functions are located in ViewController file.
the BadTime class is defined outside of any other classes
class BadTime:UIViewController{
func Alert (Message: String){
let alert = UIAlertController(title: "Mobile 3.0 Alert", message: Message, preferredStyle: UIAlertController.Style.alert)
alert.addAction(UIAlertAction(title: "OK", style: UIAlertAction.Style.default, handler: {(alert:UIAlertAction!) in exit(0)}))
self.present(alert, animated: true, completion: nil)
}
override func viewDidAppear(_ animated: Bool) {
self.Alert(Message: "Please change your time to be set automatically\nSettings->General->Date & Time->Set Automatically")
}
}
The segue is in the viewDidAppear function of the ViewController class.
self.performSegue(withIdentifier: "bad_time", sender: self)
The SceneDelegate file in which the sceneWillEnterForeground is the function that updates when ever the app becomes active
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
guard let _ = (scene as? UIWindowScene) else { return }
}
func sceneDidDisconnect(_ scene: UIScene) {
// Called as the scene is being released by the system.
// This occurs shortly after the scene enters the background, or when its session is discarded.
// Release any resources associated with this scene that can be re-created the next time the scene connects.
// The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead).
}
func sceneDidBecomeActive(_ scene: UIScene) {
// viewControl.viewDidAppear(false)
}
func sceneWillResignActive(_ scene: UIScene) {
// Called when the scene will move from an active state to an inactive state.
// This may occur due to temporary interruptions (ex. an incoming phone call).
}
func sceneWillEnterForeground(_ scene: UIScene) {
print(scene)
// Called as the scene transitions from the background to the foreground.
// Use this method to undo the changes made on entering the background.
}
func sceneDidEnterBackground(_ scene: UIScene) {
// Called as the scene transitions from the foreground to the background.
// Use this method to save data, release shared resources, and store enough scene-specific state information
// to restore the scene back to its current state.
}
}

Environment Object Method error in Scene Delegate

SO Community
I have been self-teaching myself SwiftUI and I've just come across a problem while creating an application in the Scene Delegate. When I add an environment object method to the scene delegate to assign an object to the environment of a view hierarchy, Xcode sends these errors on my let contentView = ContentView().environmentObject(delegate.myData) line of code, "Expected member name following '.' ", and "Missing argument for parameter 'appData' in call ".
I will also attach a picture of my ContentView.swift File to show where I placed my #EnviromentObject reference within the body Content View. If you guys have any questions can you please try to help me out, this has been a bum time. Thanks.
Link to Scene Delegate.Swift file : enter image description here
Link to ContentView.Swift file : enter image description here
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options
connectionOptions: UIScene.ConnectionOptions) {
let app = UIApplication.shared
let delegate = app.delegate as! AppDelegate
let contentView = ContentView()
.environmentObject(delegate.myData)
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
EnvironmentObjects are not passed via parameter like you're trying to do in your ContentView initializer. Instead, they are passed "magically" behind-the-scenes and exposed via the #EnvironomentObject property wrapper.
My suggestion is to get rid of your init method and use onAppear { } to set your contentData fields:
var body: some View {
VStack {
//...
}.onAppear {
// do initial setup
}
}
You should also make sure that you're using the same capitalization of appData -- you're using appdata sometimes, which is leading to many of your compilation errors.

is this firestore closure causing a memory leak?

So my goal is to fix this condition issue when it comes to instantiating the right viewController. I have a function that I basically use to navigate a user to the right viewController depending on the type of user and if they're logged in or not.
Here is this function :
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
let window = UIWindow(windowScene: windowScene)
self.window = window
let auth = Auth.auth()
let actualuser = Auth.auth().currentUser
auth.addStateDidChangeListener { (_, user) in
if user != nil {
db.document("student_users/\(actualuser?.uid)").getDocument { (docSnapshot, error) in
if error != nil {
print("\(error)")
} else {
let docSnap = docSnapshot?.exists
guard docSnap! else {
let alreadyLoggedInAsASchoolViewController = self.storyboard.instantiateViewController(withIdentifier: Constants.StoryboardIDs.SchoolEventDashboardStoryboardID) as! SchoolTableViewController
let navigationizedSchoolVC = UINavigationController(rootViewController: alreadyLoggedInAsASchoolViewController)
self.window!.rootViewController = navigationizedSchoolVC
self.window!.makeKeyAndVisible()
return
}
let alreadyLoggedInAsAStudentViewController = self.storyboard.instantiateViewController(withIdentifier: Constants.StoryboardIDs.StudentEventDashboardStoryboardID) as! StudentSegmentedTableViewController
let navigationizedVC = UINavigationController(rootViewController: alreadyLoggedInAsAStudentViewController)
self.window!.rootViewController = navigationizedVC
self.window!.makeKeyAndVisible()
}
}
} else {
let notLoggedInAtAll = self.storyboard.instantiateViewController(withIdentifier: Constants.StoryboardIDs.GothereMainMenuStoryboardID) as! GothereMainMenuViewController
let navMainMenu = UINavigationController(rootViewController: notLoggedInAtAll)
self.window!.rootViewController = navMainMenu
self.window!.makeKeyAndVisible()
}
}
}
I also have the exact block of code like this for the sceneDidEnterForeground for push notification purposes. Now the issue is when I run the simulator and launch the app for the first time, the correct viewController will show up, but when I logout as a school user and login as a school user in that same simulator session, the wrong viewController (aka the viewController of the other type of user) shows up.
Not that it would be a real situation in production where a student user would just have access to a school user's account and log in like that in the same scene session, but still, better to be safe than sorry. So this leads to me ask, is this a memory leak or a completely different issue?
I also get this error :
Your query is based on the variable actualuser, which looks like it is only set once, when the scene is first set up. Inside the state change callback, it's never updated.
So, when you log out, then log back in as a different user, that initial value of actualuser will be used, explaining why you see the wrong view controller. Then, when you run the app again and the scene is set up, actualuser gets set to the auth().currentUser again, showing you the correct view controller.
The solution here is to base your query on the current (and current) user.
Something like:
db.document("student_users/\(user.uid)")
(Instead of checking user != nil, do an optional binding with let user = user and then you can avoid the ? unwrapping)
This is not, by the way, a memory leak, which is a different type of issue: https://en.wikipedia.org/wiki/Memory_leak

How to send parameters in Firebase Dynamic Links

I am developing an iOS app with Swift.
I want to send a parameter with Firebase Dynamic Links.
I created a Dynamic Link called https://test.page.link/test1.
I want to attach parameters as below.
https://test.page.link/test1?id=000000
How can I get this parameter when I click on the link and launch the app?
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
// Create the SwiftUI view that provides the window contents.
let rootView = ListView()
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: rootView)
self.window = window
window.makeKeyAndVisible()
}
guard let userActivity = connectionOptions.userActivities.first(where: { $0.webpageURL != nil }) else { return }
print("url: \(userActivity.webpageURL!)") // <- No parameters could be obtained from here.
}
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
print("url: \(userActivity.webpageURL!)") // <- No parameters could be obtained from here.
}
...
}
So you can not pass parameters by short-version dynamic link.
https://PROJECTNAME.page.link/LINKNAME is short-version dynamic link.
You can pass get-query-parameters by using long-version dynamic link.
Here is an example for passing {param1: 'one', param2: 'two'} to app.
https://PROJECTNAME.page.link/?link=https%3A%2F%2FPROJECTNAME.page.link/LINKNAME%3Fparam1%3Done&param2%3Dtwo&apn=XXXXXX&isi=XXXXX&ibi=XXXXXX
You can find your long-version dynamic link by clicking the ⋮ menu next to the link on the console, going in Link Details and using your 'Long Dynamic Link'. Add parameters to the inner link and they will come through in the app.

didRegisterForRemoteNotificationsWithDeviceToken does not get triggered - Push Notifications not working

I've spent hours now trying to figure out why didRegisterForRemoteNotificationsWithDeviceToken is not being called.
It worked before. I didn't touch it in weeks. Now stopped working.
Here's my setup:
Got didRegisterForRemoteNotificationsWithDeviceToken sitting in SceneDelegate
Have declared SceneDelegate to be the UNUserNotificationCenterDelegate
I'm setting UNUserNotificationCenter.current().delegate = self in func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions)
I'm calling UNUserNotificationCenter.current().requestAuthorization { granted, error in... from one of the UIViewControllers in my App, specifically in viewDidLoad of that Controller - I get the Authorization Pop-up and when accepting I get a true back for granted
Within UNUserNotificationCenter.current().getNotificationSettings { settings in ... a check for settings.authorizationStatus == .authorized returns true
Push Notifications are added as a capability for the App in "Signing & Capabilities" and the entitlement file has been created. I've even already deleted and re-created the entitlement file - just to be sure...
I've made sure to run the app on a real device (both via Xcode as well as via TestFlight) - so the delegate not being called is not related to the App running in the Simulator.
Have checked if didFailToRegisterForRemoteNotificationsWithError gets called instead at least - but it doesn't.
I have tried with a second physical device which I have never used for testing before.
I've even checked the status of the APNS Sandbox here: https://developer.apple.com/system-status/
The didReceive delegate gets called though if I'm sending a test notification to simulator via terminal command xcrun simctl push... and I'm getting the notification.
Provisioning profile is managed by Xcode and the specified AppID is configured for Push Notifications. Certificates are set up in apple developer account.
Here's how I'm requesting Authorization from the user within viewDidLoad of one of my Apps UIViewControlles
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, error in
DispatchQueue.main.async {
if (granted) {
//self.processInitialAPNSRegistration()
UIApplication.shared.registerForRemoteNotifications()
UserDefaults.standard.set(true, forKey: "pushNotificationsEnabled")
}
print("permission granted?: \(granted)")
}
}
And here's the didRegisterForRemoteNotificationsWithDeviceToken delegate method sitting inside SceneDelegate. Note: I'm also setting UNUserNotificationCenter.current().delegate = self in scene willConnectTo and declaredSceneDelegateto implement theUNUserNotificationCenterDelegate` protocol.
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let token = deviceToken.map { String(format:"%02.2hhx", $0) }.joined()
print("didRegisterForRemoteNotificationsWithDeviceToken GOT CALLED - APNS TOKEN IS: \(token)")
self.apiService.setAPNSToken(apnsToken: token, completion: {result in
switch result {
case .success(let resultString):
DispatchQueue.main.async {
UserDefaults.standard.set(token, forKey: "apnsToken")
print(resultString, " TOKEN IS: \(token)")
}
case .failure(let error):
print("AN ERROR OCCURED: \(error.localizedDescription)")
}
})
}
Whatever I do, didRegisterForRemoteNotificationsWithDeviceToken is not getting triggered. I'm running out of ideas on what is going wrong.
Where is the error? How can I get didRegisterForRemoteNotificationsWithDeviceToken to execute again?
I think you have gone wrong in the second step didRegisterForRemoteNotificationsWithDeviceToken, didFailToRegisterForRemoteNotificationsWithError delegates are available in UIApplicationDelegate not in UNUserNotificationCenterDelegate
Add UIApplicationDelegate to your SceneDelegate class and in willConnectTo function set the delegate as:
UIApplication.shared.delegate = self
I hope this works... let me know if you still see an issue.
Have you made sure that you use the right provisioning profile which is using a push notification enabled AppId? This one is missing in your steps.
As I am not eligible to put comments yet, posting this in answer.