ViewController appears mulitple times when creating a navigationController programmatically in swift - ios

I have been creating my app completely from scratch programmatically, without using Storyboards.
My app is integrated with Firebase, and uses Facebook login.
My setup is fairly simple:
Launch the app -> takes you to the first VC called WelcomeViewController.
There is a check that happens in the viewDidLoad method to see if a user is already signed in and exists. If there is, it sends you straight to the second VC called FilmsViewController
The FilmsViewController is a collectionViewController that displays films. The user can press a film, and it takes them to more information about that film.
(For reference, I am already signed in with Facebook in my app)
I have a current issue, where when step 2 above happens, it transitions to the FilmsViewController, but it does it like 2 or 3 times. So you see the new VC appear like 2 or 3 times, then the content loads. If you press the Back button in the nav bar, it takes you back through the 2 or 3 viewControllers that it loaded before taking you back to the WelcomeViewController.
I have set my views up as follows.
In AppDelegate.swift:
var window: UIWindow?
var navController: UINavigationController?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
navController = UINavigationController()
let firstViewController: WelcomeViewController = WelcomeViewController()
self.navController!.pushViewController(firstViewController, animated: true)
window = UIWindow(frame: UIScreen.main.bounds)
window?.rootViewController = navController
window?.makeKeyAndVisible()
return true
}
In the WelcomeViewController in the viewDidLoad:
FIRAuth.auth()?.addStateDidChangeListener { auth, user in
if let user = user {
// User is signed in.
// Direct the user to the home screen
let toFilmListVC = FilmsViewController(collectionViewLayout: UICollectionViewFlowLayout())
self.navigationController?.pushViewController(toFilmListVC, animated: true)
} else { ...
}
}
I have looked loads for a solution - and nothing. I've only found one post on this issue, where someone said the solution was to change the class name of that controller, which I have already done and it didn't change anything.
Can anyone help me resolve this, please? Thank you.

The addStateDidChangeListener is probably being called multiple times.
You should modify it to check whether a FilmsViewController has already been pushed, to prevent pushing another one:
FIRAuth.auth()?.addStateDidChangeListener { auth, user in
if let user = user {
// User is signed in.
// Direct the user to the home screen
// Only push one FilmsViewController onto the navigation stack!
var shouldPush = true
if let navigationController = self.navigationController {
for viewController in navigationController.viewControllers {
if viewController is FilmsViewController {
shouldPush = false
}
}
}
if shouldPush {
let toFilmListVC = FilmsViewController(collectionViewLayout: UICollectionViewFlowLayout())
self.navigationController?.pushViewController(toFilmListVC, animated: true)
}
} else { ...
}
}

I know the above solutions works, but to make it look extremely simple.
here is the code i wrote in my project.
let someController = SomeController.someController()
if !(self.navigationController!.viewControllers.contains(someController)){
self.navigationController?.pushViewController(someController, animated:true)
}
only push the controller to navigation bar if the controller is not exist.

Related

Why doesn’t 'self.navigationController?.pushViewController(mainPage, animated: true)' work?

I am making a basic app that lets users sign up and login and they will be directed to the main page which has horizontal scrolling and paging enabled. The main view has 3 views which you can swipe left or right to go to the other pages. Here is the code where I set the root view controller.
NOTE: Only if the user has logged in this view controller will be displayed.
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
FirebaseApp.configure()
window = UIWindow(frame: UIScreen.main.bounds)
let homeScreen = MainScreen()
let navBar = UINavigationController(rootViewController: homeScreen)
window?.rootViewController = navBar
window?.makeKeyAndVisible()
return true
}
}
I want to display the main page once the user logs in, but the main page doesn't get displayed. Here is the code when the user taps on the login button, I've only given the necessary code:
// User found and verified, go to main page
let mainPage = MainScreen()
self.navigationController?.pushViewController(mainPage, animated: true)
The pushViewController doesn't work.
Make sure that before you push a viewcontroller your root view controller must be a navigation view controller otherwise it will not work.
thanks for your answers but I figured it out.
// User found and verified, go to main page
let mainPage = MainScreen()
let navController = UINavigationController(rootViewController: mainPage)
self.present(navController, animated: true, completion: nil)

Switching UIViewController without presenting it modally or stacking it on top of each other

I know there are two ways to show a new UIViewController in Swift. There are:
self.present(controllerToPresent, animated: true, completion: nil)
and
self.performSegue(withIdentifier: "controllerToPresent", sender: nil)
But both of them show the new UIViewController on top of the other. Assume I don't want to stack controllers on each other rather than just switch the controllers. The new presented UIViewController should be the new root-controller. An example for this would be a login page. Once a user logged in I don't use the login-controller anymore, so why would I like to stack the new controller on top of it. So the question is, is there a method to switch (not stacking) UIViewControllers?
Furthermore I want to know what happens to the memory that was allocated for a new instance of an UIViewController when I use one of these two functions above. I'm not sure if at some time ARC frees the memory or if I run out of memory at some time calling these functions too often.
There are many ways to do what you want...
One approach, since you comment that you want animation:
Use a "container" view as your "root" view controller
On launch, check if user is "logged in"
If not logged in, instantiate "login" view controller, and use addChildViewController() and addSubview() to show your "login" view.
Else, if already logged in on launch, instantiate "main" view controller, and use addChildViewController() and addSubview() to show your "main" view.
In the case of 3, when user completes the log=on process, instantiate "main" view controller, and use addChildViewController()... then addSubview(), but add it hidden and/or off-screen, and use a UIView animation to replace the "login" view with the "main" view... then remove the login view and controller from memory (removeFromSuperview, removeFromParentViewController, set vc reference to nil, etc).
If at some point you want to "log-off" and return to the login screen, do the same thing... instantiate loginVC, addsubview, animate subview, remove mainVC.
Specifically, for the case of (as you mentioned as an example of what are looking for):
An example for this would be a login page. Once a user logged in I
don't use the login-controller anymore
You would need to determine the desired rootViewController in the app delegate, example:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// let's assume that you impelemnted to logic of how to determine whether the user loggedin or not,
// by using 'isLoggedin' flag:
if let wnwrappedWindow = self.window {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
if isLoggedin {
let rootHomeVC = storyboard.instantiateViewControllerWithIdentifier("HomeViewController") as! HomeViewController
//...
wnwrappedWindow.rootViewController = rootHomeVC
} else {
let rootloginVC = storyboard.instantiateViewControllerWithIdentifier("HomeViewController") as! HomeViewController
//...
wnwrappedWindow.rootViewController = rootloginVC
}
}
return true
}
In case of you want to change the root view controller in the login view controller, you could implement the following code when it is a success login:
let ad = UIApplication.shared.delegate as! AppDelegate
if let window = ad.window {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let rootHomeVC = storyboard.instantiateViewControllerWithIdentifier("HomeViewController") as! HomeViewController
//...
window.rootViewController = rootHomeVC
}

How can I change my initial View Controller when the user accepts the EULA

I hope this is a simple question. I have not found anything in SO that seems to fit what I'm trying to do. I am working on my first ever project so much of this is still quite new to me. The code I have pasted is from a test project I'm using before I add code to my real one.
My initial VC is the EULA. When the user accepts, I want to change the initial VC to the log in view. In this test project, I have two views with an IBAction for a Bar Button Item that will segue the project from VC to VC2. I am trying to set up code to change the initial VC in the AppDelegate with
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Override point for customization after application launch.
let storyboard = UIStoryboard(name: "View Controller", bundle: NSBundle.mainBundle())
let licenseAccepted = ?
var vc: UIViewController?
if !licenseAccepted {
vc = storyboard.instantiateViewControllerWithIdentifier("View Controller2")
} else {
vc = storyboard.instantiateInitialViewController()
}
window = UIWindow(frame: UIScreen.mainScreen().bounds)
window?.rootViewController = vc
window?.makeKeyAndVisible()
return true
}
but this is not working. First, let licenseAccepted = ? is not correct. I found that in another set of code and I thought it might work, but no. Also, how do I set a var for if the user accepts the license in the AppDelegate? Should that be done in the initial VC, the second VC that will become the initial VC or somewhere else?
I am using Xcode 7.1 and Swift2 if that helps.
I may be way off base or I may be a keystroke or two away. I am just not getting my head around this. Your assistance will be appreciated.

Navigation Bar gone

How can I present a view controller from my AppDelegate and have a Navigation bar added to that view with a back button to the previous view? I need to do this programmatically from my AppDelegate. Currently I can push a controller from there, but it doesn't act like a segue. It doesn't add a nav bar with a back button. Now I know I should be able to add one myself, but when I do it gets hidden. Currently I'm using pushViewController(), but I imagine that's not the best way to do it.
I had something that I think is similar, if not the same:
HIGH LEVEL VIEW
The general composition of my App (thus far, and specific to the issue at hand - note: details about classes provided for context, not required for resolution) is as follows:
UIViewController (ViewController.swift) embedded in a UINavigationController
Buttons on UIViewController segue to a view with a custom class:
ExistingLocationViewController - subclass of:
UITableViewController
One of the buttons (Add New Location) in the UINavigationController's Toolbar segues to view with another custom class:
NewLocationViewController - subclass of:
UIViewController
CLLocationManagerDelegate
UITextFieldDelegate
There are a number of other items here, but I believe the above is sufficient as the foundation for the issue at hand
RESOLUTION
In order to preserve the navigation-bar (and tool-bar) going both forward and back - I have the following code in my custom classes (note: the following is Swift-3 code, you may have to adjust for Swift-2):
override func viewDidLoad() {
super.viewDidLoad()
//...
navigationController?.isNavigationBarHidden = false
navigationController?.isToolbarHidden = false
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated) // #=# not sure if this is needed
navigationController?.isNavigationBarHidden = false
navigationController?.isToolbarHidden = false
}
You could actually omit the last two lines in viewWillDisappear, or perhaps even omit the entire override function
The net result (for me) was as depicted below:
Hope that helps.
If you want add a NavigationController in appDelegate you can do it like this,in this way,your viewcontroller is load from storyboard
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Override point for customization after application launch.
self.window = UIWindow(frame: UIScreen.mainScreen().bounds)
let vc = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle()).instantiateViewControllerWithIdentifier("vc") as! ViewController
let nav = UINavigationController(rootViewController: vc)
self.window?.rootViewController = nav
self.window?.backgroundColor = UIColor.whiteColor()
self.window?.makeKeyAndVisible()
return true
}

Parse Facebook User logged in, perform segue

I am trying to skip over my login view once a user is logged in. How can I check to see if a user is logged in through Facebook while the app is starting?
I currently have the following code in a LoginViewController:
override func viewWillAppear(animated: Bool) {
var loggedIn = PFFacebookUtils.session().isOpen;
if (loggedIn) {
performSegueWithIdentifier("skipLogin", sender: self)
}
}
This does not move to my next view even after the user has clicked the "Log in with Facebook" button.
I get the following error:
Warning: Attempt to persent <_Project.HomeViewController: 0x7fa331d3af00> on
<_Project.LoginViewController: 0x7fa331f08950> whose view is not in the window hierarchy!
As discussed in chat, you have basically two options here:
Let the user "see" the animation from the login view controller to the second one. In that case you should do the push in viewDidAppear instead of viewWillAppear (where the view is not fully prepared, as the runtime warning clearly states).
If you prefer showing the final view controller immediately, without any animation, then it's better to put that logic inside your app delegate, and choose which initial view controller should be loaded from here. In that case, you're not actually performing any segue, you're just assigning one or another view controller to the main window (or your navigation controller).
Parse has the "AnyWall" sample app that implements the second logic. See here for more details: https://parse.com/tutorials/anywall#2-user-management. In particular, the chapter 2.4 is of special interest, as it explains how you can keep a user logged-in.
Simply put, here's how they did it (I've adapted their Objective-C code to Swift):
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
...
navigationController = UINavigationController()
...
// If we have a cached user, we'll get it back here
if PFFacebookUtils.session().isOpen {
// A user was cached, so skip straight to the main view
presentWallViewController(animated: false)
} else {
// No cached user, go to the welcome screen and
// have them log in or create an account.
presentLoginViewController(animated: true)
}
...
window = UIWindow(frame: UIScreen.mainScreen().bounds)
window.rootViewController = navigationController
window.makeKeyAndVisible()
return true
}
In each of the two methods present...ViewController, they use the following skeleton:
func presentxxxViewController(#animated: Bool) {
NSLog("Presenting xxx view controller")
// Go to the welcome screen and have them log in or create an account.
let storyboard = UIStoryboard(name: "Main", bundle: nil) // Here you need to replace "Main" by the name of your storyboard as defined in interface designer
let viewController = storyboard.instantiateViewControllerWithIdentifier("xxx") as xxxViewController // Same here, replace "xxx" by the exact name of the view controller as defined in interface designer
//viewController.delegate = self
navigationController?.setViewControllers([viewController], animated: animated)
}
The navigationController and window vars should be defined like this in AppDelegate:
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var navigationController: UINavigationController?
...
}
If your app also uses a navigation controller as its root view controller, you can probably use the same code.

Resources