Can't Call Segue By Identifier from AppDelegate - ios

So I have a segue from ViewController to SecondViewController. This segue is triggered by a UIButton in ViewController and the modally presents SecondViewController. The Segue's Identifier is set ("SegueIdentifier") and I am able to call the segue programatically from within my ViewController.
When I try to do the same in my AppDelegate, I get the error that the compiler can't find a segue with the Identifier I set.
let viewController = ViewController()
viewController.performSegueWithIdentifier("SegueIdentifier", sender: nil)
Again, I literally copied and pasted the performSegueWithIdentifier method call from the aforementioned method in ViewController in which I also call performSegueWithIdentifier for the same segue and it works.
Any ideas?

In my one project I am managing this situation like,
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
NSString *identifier;
BOOL isSaved = [[NSUserDefaults standardUserDefaults] boolForKey:#"loginSaved"];
if (isSaved)
{
identifier=#"home1";
}
else
{
identifier=#"dis1";
}
UIStoryboard * storyboardobj=[UIStoryboard storyboardWithName:#"Main" bundle:nil];
UIViewController *screen = [storyboardobj instantiateViewControllerWithIdentifier:identifier];
[self.window setRootViewController:screen];
return YES;
}
It is in objective c and just for reference. If it can help you :)
Update :
In swift something like,
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
var identifier: String = String()
let isSaved = NSUserDefaults.standardUserDefaults().boolForKey("loginsaved")
if isSaved
{
identifier = "home1"
}
else{
identifier = "dis1"
}
let storyboardobj: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let screen: UIViewController = storyboardobj.instantiateViewControllerWithIdentifier(identifier)
self.window!.rootViewController = screen
// Override point for customization after application launch.
return true
}
Hope this will help :)

If you are not trying to show your initial view then do like this:
UIApplication.sharedApplication().keyWindow?.rootViewController.performSegueWithIdentifier("SegueIdentifier", sender: nil)
And also it depends that you segue is set to your RootViewController. Please check that also before you do this.

Segue connection has some info with it like Source Controller, Destination Controller. So while you are calling the performSegue method from ViewController class, it will work because that class has the info of this Segue connection.
While you are calling that same Segue method from App Delegate, it will not work. Because App Delegate class doesn't have the info or definition about that Segue object.
You should also check whether the calling object comes under Navigation Controller or not.

A storyboard segue has to be tied to the specific viewController that you are trying to segue from. So you have to have an instance of the current view controller on the screen. You may use the UIApplication.sharedApplication().keyWindow?.rootViewController
Method to get access to the rootViewController in the window, however if you presented a view controller above the rootViewController and you want to perform the segue on the presented view controller you have to access the rootViewController.presentedViewController. And since you may have multiple presentedViewControllers, what you do is this:
//get the root view controller
var currentViewController = UIApplication.sharedApplication().keyWindow?.rootViewController
//loop over the presented view controllers and until you get the top view controller
while currentViewController.presentedViewController != nil{
currentViewController = currentViewController.presentedViewController
}
//And finally
currentViewController.performSegueWithIdentifier("identifier", sender: nil)

Related

Attempt to present controller whose view is not in the hierarchy

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.

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.

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.

Pushing ViewController from didReceiveLocalNotification in AppDelegate.swift

I am having problem in pushing ViewController from AppDelegate when user pressed the Notification
Below is my code, but this code Crashes because the navigationController is nil
func application(application: UIApplication, didReceiveLocalNotification notification: UILocalNotification) {
var rootViewController = self.window!.rootViewController;
let customDetailsViewController = CustomDetailsViewController();
rootViewController?.navigationController!.pushViewController(customDetailsViewController, animated: true);
}
Any idea? Thank you!!
If your navigation controller is nil, then your rootViewController (the one with the arrow in your Storyboard) is not inside a NavigationController. Can you post a screenshot of the relevant part of your Storyboard?
EDIT:
As you are using RESideController, you'll have a RootViewController not connected to anything in your Storyboard. That RootViewController conforms to ``protocol and you have some code like:
#implementation FASRootViewController
- (void)awakeFromNib
{
self.menuPreferredStatusBarStyle = UIStatusBarStyleLightContent;
self.scaleContentView = NO;
self.scaleMenuView = NO;
self.panGestureEnabled = YES;
self.contentViewController = [self.storyboard instantiateViewControllerWithIdentifier:#"contentViewController"];
self.rightMenuViewController = [self.storyboard instantiateViewControllerWithIdentifier:#"rightMenuViewController"];
self.delegate = (id<RESideMenuDelegate>)self.rightMenuViewController;
}
To "launch" your View controllers your need to add a identifier in the Storyboard
Also, this View Controller should be a UINavigationController (not your first "content" view controller)

Perform Segue on ViewDidLoad

In iOS 5 I have a Storyboard with a modal view controller, that I would like to display if its the user's first time in the app, after that I would like to skip this view controller.
I set an NSDefault key to handle this but when I check to see if this is set and then use performSegueWithIdentifier to initiate the segue, nothing happens. If i put this segue behind a button it works fine...
I answered a similar question where the developer wanted to show a login screen at the start. I put together some sample code for him that can be downloaded here. The key to solving this problem is calling things at the right time if you want to display this new view controller, you will see in the example you have to use something like this
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:#"MainStoryboard" bundle:nil];
UIViewController *vc = [storyboard instantiateViewControllerWithIdentifier:#"LoginViewController"];
[vc setModalPresentationStyle:UIModalPresentationFullScreen];
[self presentModalViewController:vc animated:YES];
}
I also have an explanation of how segues and storyboards work that you can see here
Loading in ViewDidLoad caused "under-layer" to flash. I solved this by loading my Storyboard programmatically. Thus, under Target/Main Storyboard - leave this blank. Then add the following:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
// Load Main App Screen
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:#"MainStoryboard" bundle:nil];
HomeScreenVC *homeScreenVC = [storyboard instantiateInitialViewController];
self.window.rootViewController = homeScreenVC;
[self.window makeKeyAndVisible];
// Load Login/Signup View Controller
UIViewController *mainLoginVC = [storyboard instantiateViewControllerWithIdentifier:#"MainLoginVC"];
[mainLoginVC setModalPresentationStyle:UIModalPresentationFullScreen];
[homeScreenVC presentModalViewController:mainLoginVC animated:NO];
return YES;
}
The problem is you are adding a second view to the hierarchy before the first is fully added. Try putting your code in:
-(void)viewDidAppear:(BOOL)animated{
[super viewDidAppear:animated];
// Present your modal from here
}
After [super viewDidAppear] is called you have a fully loaded view to modify.
There is no principal problem with performing segues in viewDidLoad (after the call to super of course).
The problem is performing segues before the window of the application is made visible.
The UIViewController you want to display is part of the main storyboard so it is loaded into memory before the app begins running it's code in the app delegate. In your case, the viewDidLoad is called by iOS before your application window got message: MakeKeyAndVisible.
The important part is the visibility.
Performing a segue on a view hierarchy in which the window is not visible does nothing!
You can try to do something like this:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// The window initialized with hidden = YES, so in order to perform the segue we need to set this value to NO.
// After this action, the OS will add the window.rootViewController's view as a subview of the window.
self.window.hidden = NO;
[self.window.rootViewController performSegueWithIdentifier:_IDENTIFIER_ sender:self.window.rootViewController];
// Now that the window is not hidden, we must make it key.
[self.window makeKeyWindow];
return YES;
}
UPDATE: this solution no longer works in iOS 8.
A correct way to solve your problem is to trigger the segue / present modal view controller in applicationDidBecomeActive: app delegate method or in a UIApplicationDidBecomeActiveNotification notification handler.
Apple's documentation actually advises the same:
If your app was previously in the background, you could also use it to refresh your app’s user interface.
This solution has the advantage that it works with Main storyboard loading mechanism so that you don't need to load anything manually and write unnecessary code.
I use this solution successfully on iOS 6.1, 7.0 and 7.1 and it should work on iOS 5 either.
For Swift:
dispatch_async(dispatch_get_main_queue()) {
self.performSegueWithIdentifier("toView2", sender: self)
}
For Swift 3:
DispatchQueue.main.async {
self.performSegueWithIdentifier("toView2", sender: self)
}
This is how I did it in SWIFT. This also hides the View Controller.
override func viewWillAppear(animated: Bool) {
let prefs:NSUserDefaults = NSUserDefaults.standardUserDefaults()
let isloggedIn = prefs.objectForKey("isLoggedIn") as? Bool
if (isloggedIn != false) {
self.view.hidden = true
} else {
self.view.hidden = false
}
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(true)
let prefs:NSUserDefaults = NSUserDefaults.standardUserDefaults()
let isloggedIn = prefs.objectForKey("isLoggedIn") as? Bool
if (isloggedIn != false) {
println("this should work")
self.performSegueWithIdentifier("Login", sender: self)
}
}
Swift 3
override func viewWillAppear(_ animated: Bool) {
if authPreference.isExist() == true {
self.view.isHidden = true
} else {
self.view.isHidden = false
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true)
if authPreference.isExist() == true {
navigateToSegue()
}
}
I had the same problem. Before finding this question, I solved this issue by using async in the main thread. This way, this code will be called by the UI thread right after creating the view.
dispatch_async(dispatch_get_main_queue(), ^{
[self performSegueWithIdentifier:#"segueAlbums" sender:self];
});
This code can be called in the viewDidLoad method.
Updated for Swift 3
The code snippet below allows you to load whichever viewController you want. In my case it was a TabBarController if the user had a valid facebook login token. The benefit to this solution over the other Swift 3 solution is that it's instantaneous with no screen flicker.
func applicationDidBecomeActive(_ application: UIApplication) {
if FBSDKAccessToken.current() != nil {
self.window?.rootViewController?.present((self.window?.rootViewController?.storyboard?.instantiateViewController(withIdentifier: "TabBarController"))!, animated: false, completion: nil)
}
}
The best solution is to do this:
-(void)viewDidAppear:(BOOL)animated{
[super viewDidAppear:animated];
[self performSegueWithIdentifier:#"NameSegue" sender:self];
}
I adapted #bearMountain answer for Swift 3.
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
window = UIWindow(frame: UIScreen.main.bounds)
let yourInitialVC: UIViewController? = storyboard.instantiateViewController(withIdentifier: "TermsVC")
window?.rootViewController = termsVC
window?.makeKeyAndVisible()
return true
}

Resources