viewDidAppear is delayed by 0.5 sec - ios

Greetings StackOverflow!
I'm really new to Swift development, so don't go all crazy on me please :3
I've created a login page for my mobile app (Firebase Email + Password)
I've implemented an "auto login" function - so when a user logs in for the first time, the user stays logged after closing the app & opening it.
The problem is that every time a user opens the app, there is a slight delay between viewDidLoad and viewDidAppear -> resulting in that every time the app opens, you can see the login screen for about 0.4 sec until it automatically sings the user in.
Once the user logs in the user are segued (no animation) to my UITabBarController.
I've provided the code for my LoginViewController.
LoginViewController is my first Controller the app loads in.
Best regards
class LoginViewController: UIViewController {
#IBOutlet weak var loginEmail: UITextField!
#IBOutlet weak var loginPassword: UITextField!
#IBOutlet weak var loginBorder: UIButton!
let userDefault = UserDefaults.standard
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
if userDefault.bool(forKey: "usersignedin") {
performSegue(withIdentifier: "login", sender: self)
}
//Border color button//
loginBorder.layer.borderColor = UIColor.white.cgColor
//Border color button//
//Hide Keyboard Use
self.hideKeyboardWhenTappedAround()
//Hide Keyboard Use
//start padding function for login
addPaddingAndBorder(to: loginEmail)
addPaddingAndBorder(to: loginPassword)
//start padding function for login
}
#IBAction func loginButton(_ sender: UIButton) {
Auth.auth().signIn(withEmail: loginEmail.text!, password: loginPassword.text!) { (user, error) in
if user != nil {
self.userDefault.set(true, forKey: "usersignedin")
self.userDefault.synchronize()
self.performSegue(withIdentifier: "login", sender: self)
} else {
let alert = UIAlertController(title: "Invalid Email or Password", message: nil, preferredStyle: .alert)
let okButton = UIAlertAction(title: "Ok", style: .default, handler: nil)
alert.addAction(okButton)
self.present(alert, animated: true, completion: nil)
}
}
}
}

We're doing something similar to this in my current project, but we decide which controller to show first in the AppDelegate.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
var storyboard: UIStoryboard!
if isLoggedIn {
storyboard = UIStoryboard(name: "main", bundle: nil)
}
else {
storyboard = UIStoryboard(name: "login", bundle: nil)
}
let viewController = storyboard.instantiateInitialViewController()!
viewController.loadViewIfNeeded()
window.rootViewController = viewController
}
This has the advantage that when the app loads it immediately take you to the correct ViewController without showing the wrong one briefly first.

This is correct system behavior. ViewDidLoad will always be called much sooner than ViewDidAppear. IF you want to change the base view controller based on userDefault.bool(forKey: "usersignedin"), add that logic to where you set the main view controller and swap them if already signed in.

Change the order of view controllers. Always load your UITabBarController first (as initial controller). Then in your UITabBarController viewDidLoad, check if the user is logged-in or not. If not, then show the login screen. This way your UITabBarController is always loaded first and then based on auto-logn condition, your login screen is displayed. So when the user is auto looged-in then your delay issue will not come.
I have used this concept in all of my apps and it works fine.

Move the call to performSegue(withIdentifier:sender:) to viewWillAppear(_:) instead of viewDidAppear(_:) in case usersignedin = true.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if userDefault.bool(forKey: "usersignedin") {
performSegue(withIdentifier: "login", sender: self)
}
}
Also, the response of signIn method is not received on the main thread. Since you're not updating the UI on the main thread, that's why it is resulting in the delay.
Embed the UI part in closure of signIn method in DispatchQueue.main.async
Auth.auth().signIn(withEmail: loginEmail.text!, password: loginPassword.text!) { (user, error) in
if user != nil {
self.userDefault.set(true, forKey: "usersignedin")
self.userDefault.synchronize()
DispatchQueue.main.async { //here..........
self.performSegue(withIdentifier: "login", sender: self)
}
}
else {
let alert = UIAlertController(title: "Invalid Email or Password", message: nil, preferredStyle: .alert)
let okButton = UIAlertAction(title: "Ok", style: .default, handler: nil)
alert.addAction(okButton)
DispatchQueue.main.async { //here..........
self.present(alert, animated: true, completion: nil)
}
}
}

viewDidAppear(_:) happens right after the view appears. I suggest moving your code from viewDidAppear(_:) to viewWillAppear(_:).
you can also add your login code in the viewDidLoad() because this method is called even before viewWillAppear(_:).
in your case your code will be:
override func viewDidLoad() {
super.viewDidLoad()
if userDefault.bool(forKey: "usersignedin") {
performSegue(withIdentifier: "login", sender: self)
}
}
More info:
when a view is loading, there are different methods which get called in a specific order:
viewDidLoad() (once and only when the object is being created.)
viewWillAppear(_:)
[View appears on the screen]
viewDidAppear(_:)
when you push another view controller over this one, this view controller won't be removed from the view controllers' stack. the important thing is when you come back to this view controller, this time viewDidLoad() won't get called. so only these methods get called again in this order:
viewWillAppear(_:)
[View appears on the screen]
viewDidAppear(_:)

Best way is to decide in AppDelegate which controller to load based on Login Status
let navigationController = UINavigationController()
navigationController.navigationBar.barStyle = .black
let storyboard = UIStoryboard(name: "Main", bundle: nil)
if UserDefaults.standard.object(forKey: "Token") != nil{
let initialViewController = storyboard.instantiateViewController(withIdentifier: "HomeViewController")
navigationController.viewControllers = [initialViewController]
} else{
let initialViewController = storyboard.instantiateViewController(withIdentifier: "LoginViewController")
navigationController.viewControllers = [initialViewController]
}
navigationController.navigationBar.barTintColor = UIColor.black
navigationController.navigationBar.tintColor = UIColor.white
navigationController.navigationBar.titleTextAttributes = [NSAttributedString.Key.foregroundColor : UIColor.white]
self.window?.rootViewController = navigationController
self.window?.makeKeyAndVisible()

Related

How to present a view controller before main one is loaded?

I have a view controller but in the viewDidLoad() method I call another method named authenticateUserAndConfigureView() in which when the current user is nil it presents another view controller. However, it presents with it with a little delay - it firstly loads the main view controller and then the second one from the authenticateUserAndConfigureView() method.
Why is that?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
authenticateUserAndConfigureView()
setUpElements()
}
func setUpElements() {
// Style the elements
Utilities.styleFilledButton(signUpButton)
Utilities.styleHollowButton(loginButton)
}
func authenticateUserAndConfigureView() {
if Auth.auth().currentUser == nil {
print("No user signed in..")
} else {
print("User is already signed in.")
let navController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "HomeNVC")
navController.modalPresentationStyle = .fullScreen
self.present(navController, animated: true, completion: nil)
}
}
You can insert this line inside viewWillAppear
override func viewWillAppear(_ animated:Bool) {
super.viewWillAppear(animated)
authenticateUserAndConfigureView()
}
OR make this check before presenting the MainVC and according to it present the suitable vc

How to hide pop up view on previous page when pressing back button on swift

I have this kind of flow page :
Setting Page --> Enter Password --> Go to Page X
So in setting page, in order to go to page X, I have to enter a password using pop up view.
When I'm in page X and i want to go back to previous page, I go to enter password pop up view, instead of the setting page.
How do I skip that page?
My pop up view code is something like this :
let btn = storyboard?.instantiateViewController(withIdentifier: "PopUpView") as! PopUpView
addChild(btn)
btn.view.frame = view.frame
view.addSubview(btn.view)
btn.didMove(toParent: self)
I'm fairly new here, any help will be appreciated.
Thankyou.
use the settings view controller's present function to open the popup.
//In your settings view
func openPopup() {
let btn = storyboard?.instantiateViewController(withIdentifier: "PopUpView") as! PopUpView
self.present(btn, animated: true, completion: nil)
}
When the user clicks ok, call dismiss on your popup view and use the closure to initiate the page opening to X
//In your 'PopUpView' On the OK button action of your popup
func didTouchOk() {
self.dismiss(animated: true) { [weak self] in
guard let self = self else { return }
//Put your open page X code here
let XView = storyboard?.instantiateViewController(withIdentifier: "XViewController") as! XView
self.present(XView, animated: true, completion: nil)
}
}
if you are using navigationController:
Declare a protocol in your PopUpView
protocol popupDelegate: class {
func userDidTouchOkInPopup()
}
create a weak var in your PopUpView
weak var delegate: popupDelegate? = nil
In your settings viewcontroller:
Assign delegate to popup before pushing
func openPopup() {
let btn = storyboard?.instantiateViewController(withIdentifier:
"PopUpView") as! PopUpView
btn.delegate = self
self.navigationController?.pushViewController(btn, animated: true)
}
Implement an extension for this protocol
extension SettingsViewController: popupDelegate {
func userDidTouchOkInPopup() {
self.navigationController?.popViewController(animated: true)
let XView = storyboard?.instantiateViewController(withIdentifier:
"XViewController") as! XView
self.navigationController?.pushViewController(XView, animated: true)
}
}
Modify the Ok action in your PopUpView
//In your 'PopUpView' On the OK button action of your popup
func didTouchOk() {
self.delegate?.userDidTouchOkInPopup()
}
If you are in navigationController hierarchy, use navigationController.popToViewController to go to the specific ViewController.
In ViewDidLoad, hide the present backButton and create a new One and associate action with it.
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.hidesBackButton = true
let newBackButton = UIBarButtonItem(title: "Back", style: .plain, target: self, action: #selector(backButtonFunction))
self.navigationItem.leftBarButtonItem = newBackButton
}
Inside backButtonFunction, you can put the code
#objc func backButtonFunction(){
if let navController = self.navigationController {
for controller in navController.viewControllers {
if controller is SettingsViewController { // Change name of ViewController accordingly
navController.popToViewController(controller, animated:true)
break
}
}
}
}

how to enable the customview controller with view after dismiss Second viewcontroller in swift

I have two view controller
imag
1.first view controller is main and second is CustomAlertviw controller
main view controller is like above image, when I click continues am showing the customalertview controller, like image. when click the enter pin button I have to dismiss view contoller and show the view controller with uiview on main view controller but tried, it's showing the view controller, there one more custom view is not showing it crashing my app
main view code
#IBAction func backButtonClicked(_ sender: UIButton) {
let myAlert = UIStoryboard(name: "CustomAlertview", bundle: nil).instantiateViewController(withIdentifier: "CustomAlertViewController") as? CustomAlertViewController
myAlert?.modalPresentationStyle = UIModalPresentationStyle.overCurrentContext
myAlert?.modalTransitionStyle = UIModalTransitionStyle.crossDissolve
self.present(myAlert!, animated: true, completion: nil)
}
Customalertview controller
#IBAction func enterPinButtonClick(_ sender: Any) {
self.dismiss(animated: true, completion: nil)
let myAlert = UIStoryboard(name: "ScanAndPay", bundle: nil).instantiateViewController(withIdentifier: "ScanAndPayDetailsEntryViewController") as? ScanAndPayDetailsEntryViewController
myAlert?.modalPresentationStyle = UIModalPresentationStyle.overCurrentContext
myAlert?.modalTransitionStyle = UIModalTransitionStyle.crossDissolve
self.present(myAlert!, animated: true, completion: nil)
myAlert.valuespass()
}
inside main view controller one function calling from the customalrerview
func valuespass()
{
DispatchQueue.main.async {
self.authType = 1
self.authStatus = false
print("this is calling")
self.pinStatusLabel.text = "Please enter a 4-digit Security PIN"
}
when I am calling this function from the custom alert view application is crashing and showing the hread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value this error. how to can show the all information after dismissing the custom view controller.
You are almost done. present ScanAndPay from FirstView instead of CustomAlertview.
I have done using Delegate as I think it will be fit in this scenario. Follow below steps -
Step 1:
In FirstViewController ,
Add - CustomAlertviewDelegate something like this -
class FirstViewController: UIViewController, CustomAlertviewDelegate {
Now replace your backButtonClicked method -
#IBAction func backButtonClicked(_ sender: UIButton) {
let myAlert = UIStoryboard(name: "CustomAlertview", bundle: nil).instantiateViewController(withIdentifier: "CustomAlertViewController") as? CustomAlertViewController
myAlert?.modalPresentationStyle = UIModalPresentationStyle.overCurrentContext
myAlert?.modalTransitionStyle = UIModalTransitionStyle.crossDissolve
myAlert.delegate = self
self.present(myAlert!, animated: true, completion: nil)
}
Add a new Delegate method of CustomAlertviewDelegate -
func ScanAndPayView(){
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
let myAlert = UIStoryboard(name: "ScanAndPay", bundle: nil).instantiateViewController(withIdentifier: "ScanAndPayDetailsEntryViewController") as? ScanAndPayDetailsEntryViewController
myAlert?.modalPresentationStyle = UIModalPresentationStyle.overCurrentContext
myAlert?.modalTransitionStyle = UIModalTransitionStyle.crossDissolve
self.present(myAlert!, animated: true, completion: nil)
}
}
Step 2:
Now time for CustomAlertview ,
Add protocol for delegate top of the Class -
protocol CustomAlertviewDelegate: class {
func ScanAndPayView()
}
class CustomAlertview: UIViewController{
weak var delegate : CustomAlertviewDelegate!
...
override func viewDidLoad() { // Rest of your code
}
Now time to dismiss current view controller and notify FirstViewController for presenting ScanAndPay view Controller -
#IBAction func enterPinButtonClick(_ sender: Any) {
delegate.ScanAndPayView()
self.dismiss(animated: true, completion: nil)
}
Hope it will help you to achieve what you want.

Switching the ViewController with button doesn't shows the NavigationBar in second ViewController

I want to create a page were you press the "Login" button(witch is located on NavigationBar, left item) and after is showing the second page(aka home page) were user can sign out. In the same time I want to remember that the user is logged in, using UserDefault.
Here is my AppDelegate file:
import UIKit
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
theViewController(controllerIs: LoginPage())
return true
}
fileprivate func theViewController(controllerIs: UIViewController) {
window = UIWindow(frame: UIScreen.main.bounds)
window?.makeKeyAndVisible()
window?.rootViewController = UINavigationController(rootViewController: controllerIs)
}
}
The Login view:
import UIKit
class LoginPage: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .green
navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Login", style: .plain, target: self, action: #selector(handleLogin))
}
#objc private func handleLogin() {
let root = UIApplication.shared.keyWindow?.rootViewController
root?.present(SignOutPage(), animated: true, completion: {
//some code here
})
}
}
The Sign Out view:
import UIKit
class SignOutPage: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .yellow
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Sign Out", style: .plain, target: self, action: #selector(handleSignOut))
}
#objc private func handleSignOut() {
let root = UIApplication.shared.keyWindow?.rootViewController
root?.present(LoginPage(), animated: true, completion: {
//some code here
})
}
}
The result is this:
and the sign out view
instead of
There are 2 things in going forward in navigation hierarchy.
Navigation Stack only follows in case of pushViewController. In case of present modally you need to initiate new navigation stack
let navVC = UINavigationController.init(rootViewController: viewController)
self.present(navVC, animated: true, completion: nil)
Where, viewController is the object of your new UIViewController.
Rather than using UIApplication.shared.keyWindow?.rootViewController each time you should use navigation stack( for push) and view controller( for present) as:
// Push
self.navigationController?.pushViewController(viewController, animated: true)
// Present Modally
self.present(viewController, animated: true, completion: nil)
Where, self is your view controller's object and viewController is object of new UIViewController.
So, your handleLogin() on LoginPage will be like:
// Push
#objc private func handleLogin() {
guard let vc = UIStoryboard.init(name: "Main", bundle: .main).instantiateViewController(withIdentifier: "SignOutVC") as? SignOutPage else { return }
self.navigationController?.pushViewController(vc, animated: true)
}
// Pop
#objc private func handleLogin() {
guard let vc = UIStoryboard.init(name: "Main", bundle: .main).instantiateViewController(withIdentifier: "SignOutVC") as? SignOutPage else { return }
let navVC = UINavigationController.init(rootViewController: vc)
self.present(navVC, animated: true, completion: nil)
}
And, handleSignOut() on SignOutPage will be like:
// Push
#objc private func handleSignOut() {
self.navigationController?.popViewController(animated: true)
}
// Pop
#objc private func handleSignOut() {
self.dismiss(animated: true, completion: nil)
}
NOTE: You need to set storyboard-identifier as:
When you do this:
let root = UIApplication.shared.keyWindow?.rootViewController
root?.present(SignOutPage(), animated: true, completion: {
//some code here
})
you are presenting the SignOutPage view controller modally which will put it over the root view controller.
What you are wanting to do is to push it onto the navigate controller stack so you need something like this:
navigationController?.pushViewController(SignOutPage(), animated: true)
In the same way when you sign out instead of presenting the view controller you want to pop the current one off the stack doing something like this:
navigationController?.popViewController(animated: true)
That will return the app back to the previous view controller (the login one).

Removing the view controller from memory after logging out?

I have a collection view of cells that scroll horizontally. Every time I log out, my main view controller gets dismissed to the log in screen. But when I log back in, it seems that my main view controller was never removed from memory. I say this because my collection view cells are in the same position as they were before I logged out.
I tried everything, from viewWillAppear and reloading the data, to trying to bring my log in view controller as my root window. Any suggestions?
(To be clearly demonstrate this, when you log out of Instagram (from the settings view controller), a log-in view is presented modally. When you log back in, you are presented with the Home feed, instead of the settings view controller. I want to reset the hierarchy of data)
class MainController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
let feedId = "feedId"
var allUserCategory: AllUserCategory?
override func viewDidLoad() {
super.viewDidLoad()
collectionView?.pagingEnabled = true
collectionView?.backgroundColor = UIColor(r: 250, g: 250, b: 250)
collectionView?.registerClass(AllUserCategory.self, forCellWithReuseIdentifier: feedId)
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Log Out", style: .Plain, target: self, action: #selector(handleLogout))
navigationItem.rightBarButtonItem?.tintColor = UIColor.blueColor()
navigationController?.navigationBar.translucent = false
checkIfUserLoggedIn()
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
self.collectionView?.reloadData()
}
func handleLogout() {
print("Logged out")
do {
try FIRAuth.auth()?.signOut()
} catch let logoutError {
print(logoutError)
}
allUserCategory?.users.removeAll()
let loginController = LoginController()
let loginNav = UINavigationController(rootViewController: loginController)
presentViewController(loginNav, animated: true, completion: nil)
}
}
Add NSLog in dealloc
And maybe you have some memory leak in your code(MainViewController).
Use weak reference for IBOutlet and custom delegate

Resources