Attempt to present controller whose view is not in the hierarchy - ios

I am trying to accomplish what I think is a pretty common set of steps:
When my app starts, load a home controller in a navigation controller:
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
let window = UIWindow(frame: UIScreen.mainScreen().bounds)
window.rootViewController = UINavigationController(rootViewController: HomeViewController())
window.makeKeyAndVisible()
self.window = window
return true
}
When my app loads check if the user is logged in. If not, present a registration view controller:
func applicationDidBecomeActive(application: UIApplication) {
if ref.loggedIn != nil {
// user authenticated
print(ref.userData)
} else {
// No user is signed in
let registerViewController = RegisterViewController()
if let navController = window?.rootViewController as? UINavigationController {
navController.presentViewController(registerViewController, animated: true, completion: nil)
}
}
}
In my RegisterViewController, provide a button that switches to a LoginViewController:
#IBAction func login(sender: UIButton) {
print("LOGIN")
let loginViewController = LoginViewController()
// How do I present the view controller here?
}
My question is: how can I present the LoginViewController so that calling self.dismissViewControllerAnimated(false, completion: nil) will return to my HomeViewController?
Things I've tried:
If I call self.presentViewController from the RegisterViewController then dismissing returns back to the RegisterViewController instead of HomeViewController
If I try to get a reference to rootViewController via UIApplication.sharedApplication() and present the login controller on rootViewController then I get an error "Attempt to present ... on ... whose view is not in the window hierarchy!"
Thanks!

It's obvious that you will get the error "Attempt to present ... on ... whose view is not in the window hierarchy!"
First you need to make the instance of ViewController using storyboard identifier.
Please try this :-
let storyboard = UIStoryboard(name: "YourStoryboardName", bundle: nil)
let vc = storyboard.instantiateViewControllerWithIdentifier("viewControllerToBePresented") as! UIViewController //use your class name here to cast it.
self.presentViewController(vc, animated: true, completion: nil)
EDIT
In your case you can use protocol on register screen which will get
implemented on home screen.
And in that implementation you can write code to dismiss register view
and then present Login view.

If you are trying to present a new view controller add that view controller in navigation controller and then present the navigation controller..
From the presented view controller you can dismiss to previous view controller.
I hope this will work..

In your case you can use protocol on register screen which will get implemented on home screen.
And in that implementation you can write code to dismiss register view and then present Login view.

Related

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
}

Swift: Having a Login Screen before SplitViewController

I understand that it is not allowed and UISplitViewController should always be root controller.
How can I have a login screen before splitViewController? It seems to be a very common problem.
I am new to iOS so any example will be much appreciated.
Tried updating viewDidLoad() of MasterViewController:
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
if (!appDelegate.loggedIn) {
// display the login form
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let login = storyboard.instantiateViewControllerWithIdentifier("LoginVC") as UIViewController
self.presentViewController(login, animated: false, completion: { () -> Void in
// user logged in and is valid now
//self.updateDisplay()
})
} else {
//updateDisplay()
}
But it threw me a warning: arning: Attempt to present <LoginViewController: 0x7f8ef8f12c80> on <SideBarViewController: 0x7f8ef8d9faf0> whose view is not in the window hierarchy!
Make your initial view controller the split view controller. In its viewDidLoad or viewDidAppear methods or whatever makes sense, check that the user is logged in. If the user isn't logged in, segue modally WITHOUT animation to your login view controller. This will all happen without the user noticing and will allow the storyboard setup you want.

Segue from the MasterViewController in a SplitViewController to another View Controller

I have a SplitViewController with a UITableViewController as the masterViewController and a UIViewController as the detailViewController.
When the user taps a cell, I need to push to a new UITableViewController. So I added a segue from the cell to a UITableViewController. But what happens is the UITableViewController gets added to the masterViewController's stack.
How can I push to a whole new UITableViewController from the masterViewController?
Here is a simple example how I approach such functionality (I created a new Master-Detail Application):
Storyboard:
Notice that the root VC is now a UINavigationController. Therefore AppDelegate must be changed accordingly:
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Override point for customization after application launch.
let navCtr = self.window!.rootViewController as UINavigationController
let splitViewController = navCtr.visibleViewController as UISplitViewController
let navigationController = splitViewController.viewControllers[splitViewController.viewControllers.count-1] as UINavigationController
navigationController.topViewController.navigationItem.leftBarButtonItem = splitViewController.displayModeButtonItem()
splitViewController.delegate = self
return true
}
And then finally in MasterViewController add this:
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
tableView.deselectRowAtIndexPath(indexPath, animated: true)
if indexPath.row % 2 == 0 {
self.performSegueWithIdentifier("showDetail", sender: nil)
} else {
let appDelegate = UIApplication.sharedApplication().delegate as AppDelegate
if let let rootCtr = appDelegate.window?.rootViewController {
let storyboard = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle())
let newSplit = storyboard.instantiateViewControllerWithIdentifier("SplitVC") as UISplitViewController
/// Because of Apple's "Split View Controllers cannot be pushed to a Navigation Controller"
let yetAnotherNavCtr = UINavigationController(rootViewController: newSplit)
rootCtr.presentViewController(newSplit, animated: true, completion: nil)
}
}
}
Important notes:
If you create new MasterDetail Application from the template, you have to disconnect the showDetail segue, because it is directly linked to the cell's selected callback. If you want to preserve that functionality as well, simply connect it again not from the cell itself, but from the whole VC. To be able to use it as in my funky didSelect... method that performs the showDetail segue on even rows.
The Split View presentation will work only once - I haven't implemented the whole rootViewController replacement - the lldb will complain saying: Attempt to present UISplitViewController on UINavigationController whose view is not in the window hierarchy! if you'll try to do it for the second time. But that's really up to your requirements for how you want the app to behave.
Name the SplitView Controller in Storyboard "SplitVC" (Storyboard ID) if you want to present the Split View Controller like I am doing in my code.
I got it! First I should mention Michal's answer helped me to get an idea and point me in the right direction so thanks to him.
What I did was simple actually. Before I had a View Controller with a container view embedding the split view controller. I simply went ahead and embedded that view controller in a navigation controller.
Then I added the view controller I want to segue to in the storyboard but no segue attached to it. I'm doing it programatically in the masterViewController's didSelectRowAtIndexPath method.
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
if let rootVC = appDelegate.window?.rootViewController {
let storyboard = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle())
let mapVC = storyboard.instantiateViewControllerWithIdentifier("MapVC") as! UIViewController
rootVC.showViewController(mapVC, sender: nil)
}
I get a reference to the rootViewController which is a navigation controller through the AppDelegate and I push the new view controller I want to it's stack.
That's it! Here's a demo project in case anyone's interested.

Present specific view controller in didReceiveRemoteNotification with Swift

When a user taps a push notification, I want them to be taken to a specific table view controller in my app. This table view controller is embedded in a navigation controller, which is embedded in a tab bar controller (my root view controller). The image shown below visualizes this.
The root view Tab Bar Controller has a storyboard ID of "HomeVC" and a class name of "HomeViewController", the Navigation Controller has a storyboard ID of "SettingsNavigationVC", and the Table View Controller has a storyboard ID of "SettingsTableVC" and a class name of "SettingsNavigationVC".
I have push notifications working. By working, I mean I can send a message from a device and receive it on another device, but when the receiver taps the notification I can't seem to get any other view controller to open other than the root view controller. According to the push notification guide I'm using, I'm using the following code in the didReceiveRemoteNotification method:
func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject: AnyObject]) {
let rootVC = self.window!.rootViewController
if PFUser.currentUser() != nil {
let settingsTableVC = SettingsTableViewController()
rootVC?.navigationController?.pushViewController(settingsTableVC, animated: false)
}
}
What am I doing wrong, or what must I do to present the right view controller?
Try this
let storyboard = UIStoryboard(name: "YourStoryboardName", bundle: nil)
let rootVC = storyboard.instantiateViewControllerWithIdentifier("HomeVC") as! UITabBarController
if PFUser.currentUser() != nil {
rootVC.selectedIndex = 2 // Index of the tab bar item you want to present, as shown in question it seems is item 2
self.window!.rootViewController = rootVC
}
In order to push to the specific view controller, you must replace below line:
Yours:
rootVC?.navigationController?.pushViewController(settingsTableVC, animated: false)
Mine:
rootVC?.visibleViewController.navigationController?.pushViewController(settingsTableVC, animated: true)
And for presenting any view controller, you must use this:
rootVC?.visibleViewController.navigationController?.presentViewController(settingsTableVC, animated: false, completion: nil)

Referencing TabbarController created from StoryBoard in AppDelegate?

I have a tabbar app with an initial login screen. The tabbarController is set as the initial view in Storyboard with 1 VC that has a navigationController also embed.
I have a loginVC instantiated and set as rootViewController in my AppDelegate. After the user has successfully sign in from the loginVC, I need to switch over to the tabbarController. Would I try to get a reference to the tabbarcontroller and set it as the new rootviewcontroller? If so, I'm having a hard time figuring out how to do this:/
AppDelegate
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
if NSUserDefaults.standardUserDefaults().objectForKey("OAuth") == nil {
self.window = UIWindow(frame:UIScreen.mainScreen().bounds)
var storyboard = UIStoryboard(name: "Main", bundle: nil)
var loginVC = storyboard.instantiateViewControllerWithIdentifier("LOGIN_VC") as LoginVC
self.window?.rootViewController = loginVC
self.window?.makeKeyAndVisible()
}
return true
}
This method gets called after user has successfully signed in
func dismissLoginVC() {
var tabbarController = self.storyboard?.instantiateViewControllerWithIdentifier("TABBAR") as UITabBarController
self.presentViewController(tabbarController, animated: true, completion: nil)
let appDelegate: AppDelegate = UIApplication.sharedApplication().delegate as AppDelegate
appDelegate.window?.rootViewController = tabbarController
}
I know the problem with this is it just created a new tabbarcontroller, rather than referencing the existing tabbarController that was set as the initialView in storyboard. This seems to work but it is missing other items from the navigation bar.
Thanks for pointing me in the right direction!
I think you should change your app structure. Keep the tab bar controller as the initial view controller in the storyboard, and present modally (with no animation) the login controller from the viewDidAppear method of the controller in the first tab -- it will be the first thing the user sees. When you dismiss it, you will be back to that controller in the first tab. With this approach, you don't need any code in the app delegate.

Resources