when need to switch the rootViewController - ios

I've been working on a Swift project and I have two view controllers, the login view controller & the home view controller. When a user launches the app, I want to display the login view controller if the user is not logged in, on the other hand, if the user is logged in, I want to display the home view controller.
So the flow is gonna be something like this.
When the user is not logged in, display
LoginViewController
HomeViewController
When the user is already logged in, display
HomeViewController
In the scene delegate, I've written
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let scene = (scene as? UIWindowScene) else { return }
window = UIWindow(frame: scene.coordinateSpace.bounds)
window?.windowScene = scene
window?.rootViewController = HomeViewController() or LoginViewController() depending on the user's login status
window?.makeKeyAndVisible()
}
I was wondering if I should apply the HomeViewController as a rootviewcontroller regardless of the user's login status (and maybe present loginVC on the homeVC when the user is not logged in), or I should switch the view controller depending on the user's login status.
So, in this case, what is the point of switching rootviewcontroller? and why it is (or isn't important) to switch the root view controller?
Is there anything I should consider when I apply view controller to the root viewcontroller property?

// 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 }
// if user is logged in before
if let loggedUsername = UserDefaults.standard.string(forKey: "username") {
window?.rootViewController = HomeViewController()
} else {
// if user isn't logged in
window?.rootViewController = LoginViewController()
}
}

I think there can be another cases, like RootVC is a container ViewContoller consists of HomeVC and LoginVC.
example)
final class RootVC: UIViewController {
private let loginVC = LoginVC()
private let homeVC = HomeVC()
override func viewDidLoad() {
super.viewDidLoad()
addChild(homeVC)
view.addSubview(homeVC.view)
addChild(loginVC)
view.addSubview(loginVC.view)
}
func showVC() {
if isLogin {
homeVC.hide()
loginVC.show()
} else {
reverse()
}
}
}

Hi all i have one idea for set a RootViewController in SceneDelegate. First we need to create the method setViewController and variable currentScene in SceneDelegate class kindly feel free to refer the code below.
Two different viewcontroller as per your example HomeViewController, LoginViewController
import UIKit
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
var currentScene: UIScene?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let _ = (scene as? UIWindowScene) else { return }
currentScene = scene
if UserDefaults.standard.bool(forKey: "isLoggedIn") == true{
self.setRootViewController(LoginViewController())
}else{
self.setRootViewController(HomeViewController())
}
}
func setRootViewController(_ viewController: UIViewController){
guard let scene = (currentScene as? UIWindowScene) else { return }
window = UIWindow(frame: scene.coordinateSpace.bounds)
window?.windowScene = scene
window?.rootViewController = viewController
window?.makeKeyAndVisible()
}
}
class ButtonViewController: UIViewController {
lazy var button: UIButton! = {
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.backgroundColor = .darkGray
button.setTitleColor(.white, for: .normal)
button.addTarget(self, action: #selector(buttonAction), for: .touchUpInside)
button.tag = 1
button.setTitle("Tap", for: .normal)
return button
}()
override func loadView() {
super.loadView()
self.view.backgroundColor = .white
setConstraint()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
}
func setConstraint(){
self.view.addSubview(self.button)
NSLayoutConstraint.activate([
self.button.heightAnchor.constraint(equalToConstant: 60),
self.button.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width * 0.66),
self.button.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
self.button.centerYAnchor.constraint(equalTo: self.view.centerYAnchor),
])
}
#objc func buttonAction(){ }
}
class HomeViewController: ButtonViewController{
override func loadView() {
super.loadView()
self.view.backgroundColor = .red
self.button.setTitle(String(describing: HomeViewController.self), for: .normal)
}
override func buttonAction() {
let sceneDelegate = UIApplication.shared.connectedScenes.first?.delegate as! SceneDelegate
sceneDelegate.setRootViewController(LoginViewController())
}
}
class LoginViewController: ButtonViewController{
override func loadView() {
super.loadView()
self.view.backgroundColor = .green
self.button.setTitle(String(describing: LoginViewController.self), for: .normal)
}
override func buttonAction() {
let sceneDelegate = UIApplication.shared.connectedScenes.first?.delegate as! SceneDelegate
sceneDelegate.setRootViewController(HomeViewController())
}
}
If click the button view controller can be change as rootViewController.
Output:

Related

Can't show next view controller while using coordinator pattern

I am trying to use coordinator in my project.
I want to show next viewController on button click.
My code goes to navigationController.pushViewController(registrationViewController, animated: true) but nothing happens
My FirstViewController
class AuthViewController: UIViewController {
private var registrationCoordinator: RegistrationCoordinator?
...
#objc func registrationButtonPressed() {
registrationCoordinator = RegistrationCoordinator(navigationController: UINavigationController())
registrationCoordinator?.start()
}
}
My Coordinator
class RegistrationCoordinator {
private let navigationController: UINavigationController
var authViewController: AuthViewController?
//Init
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
//Functions
public func start() {
showRegistrationViewController()
}
private func showRegistrationViewController() {
let registrationViewController = RegistrationViewController()
navigationController.isNavigationBarHidden = true
registrationViewController.view.backgroundColor = .orange
navigationController.pushViewController(registrationViewController, animated: true)
}
}
My SceneDelegate
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
var authCoordinator: AuthCoordinator?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
let rootWindow = UIWindow(windowScene: windowScene)
let navigationController = UINavigationController()
authCoordinator = AuthCoordinator(navigationController: navigationController)
window = rootWindow
window?.rootViewController = navigationController
window?.makeKeyAndVisible()
authCoordinator?.start()
}
#objc func registrationButtonPressed() {
registrationCoordinator = RegistrationCoordinator(navigationController:
UINavigationController())
registrationCoordinator?.start() }
When you call your coordinator you are instantiating the navigation controller.
Then you are using your navigation controller to push a viewcontroller, but yout navigation controller isn't in the view herarchy, not in the main window, not inside other view.
In other words, your navigation controller exists, but is not part of the interface. Therefore nothing it does would be shown.
You are not passing the same Navigation Controller you use in the SceneDelegate, you are creating a new one.
You can pass to the coordinator the navigation controller of your current viewcontroller.
registrationCoordinator =
RegistrationCoordinator(navigationController:
self.navigationController?)
That, of course, assuming that your current viewcontroller has a navigation controller (and your coordinator would have to accept optionals)

iOS: Navigation Bar of a Programmatically created UINavigationController Not Expanding to Safe Area

I want the navigation bar to expand to safe area in a programmatically created UINavigationController. I'm working on a project where they create the initial view controller programmatically and setup its navigation bar in the SceneDelegate:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
window = UIWindow(windowScene: windowScene)
let initialViewController = initViewController()
let navigationCotnroller = UINavigationController(rootViewController: initialViewController)
navigationBarConfiguration(navigationCotnroller)
window?.rootViewController = navigationCotnroller
window?.makeKeyAndVisible()
}
private func initViewController () -> UIViewController {
let view_controller_to_be_returned = DeviceSearchVC()
view_controller_to_be_returned.title = "Devices"
return view_controller_to_be_returned
}
private func navigationBarConfiguration (_ controller: UINavigationController) {
controller.navigationBar.prefersLargeTitles = true
controller.navigationBar.largeTitleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white]
controller.navigationBar.titleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white]
controller.navigationBar.tintColor = .white
controller.navigationBar.backgroundColor = UIColor.systemBlue
}
It looks like this:
I want the navigation bar to expand to the safe area.
I tried something like this:
extension UIViewController: UINavigationBarDelegate{
func position(for bar: UIBarPositioning) -> UIBarPosition {
return .topAttached
}
}
but that didn't work
Just add this to the end of your navigationBarConfiguration func
if #available(iOS 13.0, *) {
let navBarAppearance = UINavigationBarAppearance()
navBarAppearance.configureWithOpaqueBackground()
navBarAppearance.backgroundColor = UIColor.systemBlue
controller.navigationBar.standardAppearance = navBarAppearance
controller.navigationBar.scrollEdgeAppearance = navBarAppearance
} else {
controller.edgesForExtendedLayout = []
}

Loading screen off centre based on launch storyboard?

I have 2 storyboards: Onboarding and Main. Everything works great as far as execution goes. The issue is that I have loading screen at the start of Main.storyboard that looks different depending on which storyboard launches.
Scenario 1: New user launches the app, it goes through Onboarding.storyboard then Main.storyboard launches and the loading screen looks as it should like this: Correct Screen
Scenario 2: User launches the app for the second time, therefore Onboarding.storyboard is not launched: Loading screen is off centre like this: Wrong Screen
Long story short: If there is no Onboarding.storyboard, it doesn't look right. The issue is only shown on iPad landscape; portrait looks fine.
All of my loading screen code is inside my first view controller of Main.Storyboard so I'm guessing that the issue is the order in which the code is called?
Onboarding.Storyboard only has 1 view controller
Main.Storyboard goes: TabBarController -> NavController -> FirstVC
I placed my SceneDelegate code below along with the loading screen code of FirstVC.
In case you want to dig in deeper, I'm using RevealingSplashView available on github. Since I don't think that's the issue I didn't get into that further.
SceneDelegate:
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
let onboardingStoryboard = UIStoryboard(name: "Onboarding", bundle: nil)
let mainStoryboard = UIStoryboard(name: "Main", bundle: nil)
var vc: UIViewController
if UserDefaults.standard.value(forKey: "firstTimer") == nil {
vc = onboardingStoryboard.instantiateInitialViewController()!
} else {
vc = mainStoryboard.instantiateInitialViewController()!
}
self.window?.rootViewController = vc
self.window?.makeKeyAndVisible()
}
FirstVC relevant code:
class firstVC: UIViewController {
var revealingSplashView : RevealingSplashView!
override func viewDidLoad() {
super.viewDidLoad()
showLoadingScreen()
}
func showLoadingScreen() {
revealingSplashView = RevealingSplashView(iconImage: UIImage(named: "Icon")!, iconInitialSize: CGSize(width: 150, height: 150), backgroundImage: UIImage(named: "loadBackground")!)
revealingSplashView.animationType = .heartBeat
revealingSplashView.startAnimation()
view.addSubview(revealingSplashView)
}
Check to see that your layout constraints are the same in both of your storyboards. I'd recommend just setting them to center horizontal and center vertical on both of them.
In your scene delegate callback, why are you calling instantiateInitialViewController() twice per storyboard?
In my iOS 12 project, I used storyboards to kick things off. In app delegate didFinishLaunching, I would override and set the window if and only if it was a new user. So my "Main" storyboard would kick in most of the time.
Same technique could be applied here. Check out the Info.plist values you need to set for UIScene storyboard to work. You can override the window if you want to show the first time onboarding, otherwise let the system instantiate your main storyboard.
Further testing revealed that if I return SceneDelegate to its "factory default" state, therefore launching Main.storyboard, I had no issues.
To fix the issue I simply changed my SceneDelegate to:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
if UserDefaults.standard.value(forKey: "firstTimer") == nil {
window = UIWindow(frame: windowScene.coordinateSpace.bounds)
window?.windowScene = windowScene
let onboardingStoryboard = UIStoryboard(name: "Onboarding", bundle: nil)
var vc: UIViewController
vc = onboardingStoryboard.instantiateInitialViewController()!
self.window?.rootViewController = vc
self.window?.makeKeyAndVisible()
}
}

Trying to find a better approach to custom tab bar

I'm trying to create a custom TabBar.
My approach so far, was to create one UIViewController (Let's call it the TabBarController), In the TabBarController, I added a childVC (Let's call it UserViewController).
I couldn't figure out a way to change the UserViewController without making the TabBarController to disappear.
I started to think maybe this is not a good option, and that there most be a better, actually working way to do so.
All I really trying to achieve is a TabBarController, separated from the UserViewController, the TabBarController will always be displayed on the bottom of the screen, and by clicking the items in it, the UserViewController will change accordingly.
I searched for hours, tried different solutions, nothing worked. Really hope you could guide me, maybe share a tutorial or article you have about this.
Create a subclass of UITabBarViewController.
class TabBarController: UITabBarController {
var previousTabIndex: Int?
override func viewDidLoad() {
super.viewDidLoad()
let viewControllerOne = ViewController()
viewControllerOne.tabBarItem.image = UIImage(named: "one")
viewControllerOne.tabBarItem.title = "One"
let viewControllerTwo = ViewController()
viewControllerTwo.tabBarItem.image = UIImage(named: "two")
viewControllerTwo.tabBarItem.title = "Two"
let viewControllerThree = ViewController()
viewControllerThree.tabBarItem.title = "Premium"
viewControllerThree.tabBarItem.image = UIImage(named: "three")
let tabBarList = [
viewControllerOne,
viewControllerTwo,
viewControllerThree
]
self.viewControllers = tabBarList.map {
let nav = UINavigationController(rootViewController: $0)
return nav
}
}
}
And then in SceneDelegate, set window.rootViewController = TabBarController()
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = TabBarController()
self.window = window
window.makeKeyAndVisible()
}
}
If you have created your project in older version of xCode, assign TabBarController() to window.rootController in didFinishLaunchingWithOptions of AppDelegate

Swift – Instantiating a navigation controller without storyboards in App Delegate

I'm rebuilding an app without storyboards and the part of it that I'm having the most trouble with is navigating view-to-view programatically. Few things are written out there which don't use storyboards, so finding an answer for this has been tough.
My problem is pretty simple. I have my ViewController and my SecondViewController and I want to push from the former to the latter.
In AppDelegate:
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Override point for customization after application launch.
window = UIWindow(frame: UIScreen.mainScreen().bounds)
window?.backgroundColor = UIColor.whiteColor()
window?.rootViewController = ViewController()
window?.makeKeyAndVisible()
return true
}
Then in ViewController.swift:
class ViewController: UIViewController, AVAudioPlayerDelegate, UITextFieldDelegate {
override func viewDidLoad() {
super.viewDidLoad()
startFinishButton.setTitle("Begin", forState: .Normal)
startFinishButton.addTarget(self, action: "moveToSecondViewController", forControlEvents: .TouchUpInside)
view.addSubview <*> startFinishButton
}
func moveToSecondViewController(sender: UIButton) {
let vc = SecondViewController()
println(self.navigationController) // returns nil
self.navigationController?.pushViewController(vc, animated: true)
}
}
Printing self.navigationController returns nil. I've tried doing:
var navController = UINavigationController() when the ViewController class is created (but outside of ViewDidLoad, right under the class declaration) and done the push using the navController var but that hasn't worked.
I'm thinking maybe the solution is to create a navigation controller in App Delegate that the whole app would use, I guess as a global variable?
My hope is that this post can serve many others who are new to Swift and want to remove storyboards from their app.
Thanks for taking a look and for your help.
In Swift 3
Place this code inside didFinishLaunchingWithOptions method in AppDelegate class.
window = UIWindow(frame: UIScreen.main.bounds)
let mainController = MainViewController() as UIViewController
let navigationController = UINavigationController(rootViewController: mainController)
navigationController.navigationBar.isTranslucent = false
self.window?.rootViewController = navigationController
self.window?.makeKeyAndVisible()
In AppDelegate
var window: UIWindow?
var navController: UINavigationController?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
navController = UINavigationController()
var viewController: ViewController = ViewController()
self.navController!.pushViewController(viewController, animated: false)
self.window = UIWindow(frame: UIScreen.mainScreen().bounds)
self.window!.rootViewController = navController
self.window!.backgroundColor = UIColor.whiteColor()
self.window!.makeKeyAndVisible()
return true
}
In ViewController
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.title = "FirstVC"
var startFinishButton = UIButton.buttonWithType(UIButtonType.System) as! UIButton
startFinishButton.frame = CGRectMake(100, 100, 100, 50)
startFinishButton.backgroundColor = UIColor.greenColor()
startFinishButton.setTitle("Test Button", forState: UIControlState.Normal)
startFinishButton.addTarget(self, action: "buttonAction:", forControlEvents: UIControlEvents.TouchUpInside)
self.view.addSubview(startFinishButton)
}
func buttonAction(sender:UIButton!)
{
println("Button tapped")
let vc = SecondViewController()
self.navigationController?.pushViewController(vc, animated: true)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
In Swift 5 and Xcode 13 there is a SceneDelegate along with the AppDelegate. So now to completely remove the storyboard from a project and embed the view controller in a navigation controller do the following:
Delete the actual storyboard in the Project Navigator
Select the project in the Project Navigator, select Target and then the General tab, then delete the storyboard from Main Interface
In the info.plist file delete the storyboard from:
Application Scene Manifest > Scene Configuration > Application Session Role > Item 0 (Default Configuration) > Storyboard Name
Then in the scene delegate change the scene function to look like this:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
let window = UIWindow(windowScene: windowScene)
let navigationController = UINavigationController(rootViewController: YourViewController())
window.rootViewController = navigationController
self.window = window
window.makeKeyAndVisible()
}

Resources