This article assumes your app will only have one scene all of the time. (ie. no multiple scenes at the same time in iPad).

At the end of this post, you will be able to implement a login screen (a navigation controller which can push to registration view), that transition into the main tab controller like this :

demo achievement

Here are some common mistakes I saw other developers did (I did it too when first starting out) :

Common mistakes

1. Presenting the main view from login view on viewDidAppear every time

You might have attempted to present or push the main view controller on the login view controller when user has logged in previously, using code like this :

// LoginViewController.swift

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    
    // if user has logged in previously, present the main view controller
    
    if let loggedUsername = UserDefaults.standard.string(forKey: "username") {
        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        let mainTabBarController = storyboard.instantiateViewController(identifier: "MainTabBarController")
        mainTabBarController.modalPresentationStyle = .fullScreen
        
        self.present(mainTabBarController, animated: true, completion: nil)
    }
}


This works but user will have to see the present/push animation every time they launch the app, if they have already logged in previously :

slow mo modal

(I used slow animation so it is easier to notice).

2. Keep presenting view controllers without dismissing

When user taps login, the main view controller is pushed/presented on top, then when user logs out, the login view controller is pushed / presented on top, and keeps repeating. This might look like a seamless experience, but it will keep the view controllers stacked without ever being released from memory, which can cause the app to run out of memory and crash eventually if user keeps on login and logout (unlikely but still).

If you never dismiss the view controllers, they will keep stacking on top like this :

stack

And memory usage will keep increasing as more view controllers are added on top of the stack, notice the memory keeps increasing when I present a view controller :

keep pushing

Changing root view controller

Instead of pushing/ presenting view controller endlessly, a better way to do this is to change the root view controller directly. Root view controller is the bottom-most view controller on the stack. If you are using storyboard, the root view controller is the initial view controller you have set in your storyboard :

initial view controller

Instead of presenting the tab bar controller on top of login view controller:

present

We are going to switch the bottom-most view controller (root view controller) from the login view to main tab view like this :

switch root

Step 1: Show the correct view controller on app launch

If the user is logged in, we want to show the Main tab bar controller as the root view controller.

If the user is not logged in, we want to show the Navigation controller that contains the login and register view controller.

Assuming you have a storyboard with a flow similar to this, with navigation controller for the login/ register VC, and a tab bar controller once user has logged in  :

storyboard

In order to access the view controllers in code, we need to assign them a storyboard identifier. Go ahead and assign storyboard ID for the navigation controller and tab bar controller :

login nav view controller

(Storyboard Identifier: "LoginNavigationController" for the navigation controller)

main tab identifier

(Storyboard Identifier: "MainTabBarController" for the tab bar controller)

After setting the storyboard identifier, head over to SceneDelegate.swift, and inside the willConnectTo session: function, add these lines :

// SceneDelegate.swift

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 }
    
    // add these lines
    let storyboard = UIStoryboard(name: "Main", bundle: nil)
    
    // if user is logged in before
    if let loggedUsername = UserDefaults.standard.string(forKey: "username") {
        // instantiate the main tab bar controller and set it as root view controller
        // using the storyboard identifier we set earlier
        let mainTabBarController = storyboard.instantiateViewController(identifier: "MainTabBarController")
        window?.rootViewController = mainTabBarController
    } else {
        // if user isn't logged in
        // instantiate the navigation controller and set it as root view controller
        // using the storyboard identifier we set earlier
        let loginNavController = storyboard.instantiateViewController(identifier: "LoginNavigationController")
        window?.rootViewController = loginNavController
    }
}


Don't have a SceneDelegate.swift file in your project? Your project might be created using Xcode 10 or earlier, no worries, in this case, you can put the code in AppDelegate.swift, inside the didFinishLaunchingWithOptions method.

// AppDelegate.swift

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    // Override point for customization after application launch.
    // add these lines
    let storyboard = UIStoryboard(name: "Main", bundle: nil)
    
    // if user is logged in before
    if let loggedUsername = UserDefaults.standard.string(forKey: "username") {
        // instantiate the main tab bar controller and set it as root view controller
        // using the storyboard identifier we set earlier
        let mainTabBarController = storyboard.instantiateViewController(identifier: "MainTabBarController")
        window?.rootViewController = mainTabBarController
    } else {
        // if user isn't logged in
        // instantiate the navigation controller and set it as root view controller
        // using the storyboard identifier we set earlier
        let loginNavController = storyboard.instantiateViewController(identifier: "LoginNavigationController")
        window?.rootViewController = loginNavController
    }
  
    return true
}


Step 2 : Change the root view controller to main tab bar controller after login

So now instead of presenting the main tab bar controller, we will change the root view controller of the app directly after login.

Let's add a function to switch root view controller in the SceneDelegate.swift file, then we can use this function in all view controllers.

// SceneDelegate.swift

func changeRootViewController(_ vc: UIViewController, animated: Bool = true) {
    guard let window = self.window else {
        return
    }
    
    // change the root view controller to your specific view controller
    window.rootViewController = vc
}


Then in your login view controller, you can call this function after user successfully logs in :

// LoginViewController.swift
@IBAction func loginTapped(_ sender: UIButton) {
    // ...
    // after login is done, maybe put this in the login web service completion block

    let storyboard = UIStoryboard(name: "Main", bundle: nil)
    let mainTabBarController = storyboard.instantiateViewController(identifier: "MainTabBarController")
    
    // This is to get the SceneDelegate object from your view controller
    // then call the change root view controller function to change to main tab bar
    (UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate)?.changeRootViewController(mainTabBarController)
}


UIApplication.shared.connectedScenes.first is used to get the first scene connected to your app, this article assumes your app has only one scene (ie. user cannot open more than one scene of your app in iPad multitasking mode). As your app has only one scene, the .first will get the one and only scene of your app.

Then the delegate is an object (which usually is the SceneDelegate) conforming to the UISceneDelegate protocol, we can cast it back to SceneDelegate object using as? SceneDelegate , then we can access the changeRootViewController method

If your changeRootViewController() function is located in AppDelegate.swift, you can call it using :

(UIApplication.shared.delegate as? AppDelegate)?.changeRootViewController(mainTabBarController)


Now when user logs in, the root view controller will switch like this :

switch to login

This transition feels abrupt, we can add some animation in the changeRootViewController to make the transition looks smoother :

// SceneDelegate.swift or AppDelegate.swift

func changeRootViewController(_ vc: UIViewController, animated: Bool = true) {
    guard let window = self.window else {
        return
    }

    window.rootViewController = vc

    // add animation
    UIView.transition(with: window,
                      duration: 0.5,
                      options: [.transitionFlipFromLeft],
                      animations: nil,
                      completion: nil)

}


You can choose from a list of preset transition :

transition list

For the code above, we will have a flip animation.

Step 3 :  Change the root view controller to login navigation controller after logout

Similar to previous step, we change the root view controller to the login navigation controller after user logs out.

// ProfileViewController.swift
@IBAction func logoutTapped(_ sender: UIButton) {
    // ...
		// after user has successfully logged out
  
    let storyboard = UIStoryboard(name: "Main", bundle: nil)
    let loginNavController = storyboard.instantiateViewController(identifier: "LoginNavigationController")

    (UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate)?.changeRootViewController(loginNavController)
}


If your changeRootViewController() function is located in AppDelegate.swift, you can call it using :

(UIApplication.shared.delegate as? AppDelegate)?.changeRootViewController(mainTabBarController)


And now we have implemented the login / logout transition flow by changing root view controller! 🙌

demo achievement