Performing a completion handler before app launches
Asked Answered
N

1

1

I am attempting to open an app in one of two ways:

  1. If the user has no UserDefaults saved, then open up a WelcomeViewController
  2. If the user has UserDefaults saved, then open up a MenuContainerViewController as a home page

In step 2, if there are UserDefaults saved, then I need to log a user in using Firebase which I have through a function with a completion handler. If step 2 is the case, I want to open MenuContainerViewController within the completion block without any UI hiccups.

Here is the code I have currently:

func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

    self.window = UIWindow(frame: UIScreen.main.bounds)

    FirebaseApp.configure()

    guard
        let email = UserDefaults.standard.string(forKey: "email"),
        let password = UserDefaults.standard.string(forKey: "password")
    else {
        // User has no defaults, open welcome screen
        let welcomeViewController = WelcomeViewController()
        self.window?.rootViewController = welcomeViewController
        self.window?.makeKeyAndVisible()
        return true
    }

    // User has defaults saved locally, open home screen of app
    let authentificator = Authentificator()
    authentificator.login(with: email, password) { result, _ in
        if result {
            let menuContainerViewController = MenuContainerViewController()
            self.window?.rootViewController = menuContainerViewController
            self.window?.makeKeyAndVisible()
        }
    }

    return true
}

Here is a video of the current UI, when I need to run the completion handler, the transition is not smooth into the app (there is a brief second with a black screen).

https://gph.is/g/Zdbjq81

Please help me figure out how to make a smooth app launch.

Norrisnorrv answered 13/11, 2019 at 20:45 Comment(3)
I presume authentificator.login() is asynchronous?Mccarty
Correct, you are rightNorrisnorrv
The answer is that an asynchronous check will have to be done after the app launches. The blank screen is to be expected, the app doesn't know what to do while it is waiting for the result. In the meantime, you need to create a UI that looks nice while making the authentication check. I'm mocking up something that may help you.Mccarty
M
1

I've had to handle situations similarly in my Firebase applications. What I typically do is make an InitialViewController. This is the view controller that is always loaded, no matter what. This view controller is initially set up to seamlessly look exactly like the launch screen.

This is what the InitialViewController looks like in the interface builder: enter image description here

And this is what my launch screen looks like: enter image description here

So when I say they look exactly the same, I mean they look exactly the same. The sole purpose of this InitialViewController is to handle this asynchronous check and decide what to do next, all while looking like the launch screen. You may even copy/paste interface builder elements between the two view controllers.

So, within this InitialViewController, you make the authentication check in viewDidAppear(). If the user is logged in, we perform a segue to the home view controller. If not, we animate the user onboarding elements into place. The gifs demonstrating what I mean are pretty large (dimension-wise and data-wise), so they may take some time to load. You can find each one below:

User previously logged in.

User not previously logged in.

This is how I perform the check within InitialViewController:

@IBOutlet var loginButton: UIButton!
@IBOutlet var signupButton: UIButton!
@IBOutlet var stackView: UIStackView!
@IBOutlet var stackViewVerticalCenterConstraint: NSLayoutConstraint!

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)

    //When the view appears, we want to check to see if the user is logged in.
    //Remember, the interface builder is set up so that this view controller **initially** looks identical to the launch screen
    //This gives the effect that the authentication check is occurring before the app actually finishes launching
    checkLoginStatus()
}

func checkLoginStatus() {
    //If the user was previously logged in, go ahead and segue to the main app without making them login again

    guard
        let email = UserDefaults.standard.string(forKey: "email"),
        let password = UserDefaults.standard.string(forKey: "password")
    else {
        // User has no defaults, animate onboarding elements into place
        presentElements()
        return
    }

    let authentificator = Authentificator()
    authentificator.login(with: email, password) { result, _ in
        if result {
            //User is authenticated, perform the segue to the first view controller you want the user to see when they are logged in
            self.performSegue(withIdentifier: "SkipLogin", sender: self)
        }
    }
}

func presentElements() {

    //This is the part where the illusion comes into play
    //The storyboard elements, like the login and signup buttons were always here, they were just hidden
    //Now, we are going to animate the onboarding UI elements into place
    //If this function is never called, then the user will be unaware that the launchscreen was even replaced with this view controller that handles the authentication check for us

    //Make buttons visible, but...
    loginButton.isHidden = false
    signupButton.isHidden = false

    //...set their alpha to 0
    loginButton.alpha = 0
    signupButton.alpha = 0

    //Calculate distance to slide up
    //(stackView is the stack view that holds our elements like loginButton and signupButton. It is invisible, but it contains these buttons.)
    //(stackViewVerticalCenterConstraint is the NSLayoutConstraint that determines our stackView's vertical position)
    self.stackViewVerticalCenterConstraint.constant = (view.frame.height / 2) + (stackView.frame.height / 2)

    //After half a second, we are going to animate the UI elements into place
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
        UIView.animate(withDuration: 0.75) {
            self.loginButton.alpha = 1
            self.signupButton.alpha = 1

            //Create correct vertical position for stackView
            self.stackViewVerticalCenterConstraint.constant = (self.view.frame.height - self.navigationController!.navigationBar.frame.size.height - self.signupButton.frame.maxY - (self.stackView.frame.size.height / 2)) / 3
            self.view.layoutIfNeeded()
        }
    }   
}
Mccarty answered 13/11, 2019 at 21:29 Comment(6)
I like this approach. Seems like a pretty simple workaround. I'll give it a shot in my app.Norrisnorrv
Let me know if anything doesn't make sense. I've been in your shoes of needing to handle an asynchronous check immediately when the app launches. A few months of development later, and I've settled on something like this (though a little more complicated). You seem familiar enough with iOS development to figure out how you can make this same view controller work for when the user signs out of your app.Mccarty
Also note: I just realized I had the two gifs/their scenarios mismatched. They should make more sense now.Mccarty
I got it to work. I even added back in a Twitter-style animation with RevealingSplashView that I left out for simplicity purposes. Thanks again for the help.Norrisnorrv
No problem, send a gif if you can :)Mccarty
gph.is/g/EJg8v0Q. Pretty smooth (this is when a user is already logged in)Norrisnorrv

© 2022 - 2024 — McMap. All rights reserved.