When using a storyboard when does the OS set the rootViewController? - ios

I was surprised to observe that within didFinishLaunchingWithOptions a call to [UIApplication sharedApplication].keyWindow.rootViewController returns nil.
I want to set some things up once on app launch where I need to reference the rvc, however this behavior means I'll have to do it elsewhere.
If it can't be done in didFinishLaunchingWithOptions then the only other choice is applicationDidBecomeActive? But with the additional irritation (a small irritation, but still you'd think it shouldn't have to be necessary) of having to have a flag to ensure the set up steps only happens once and not every time appliationDidBecomeActive is called.
Is there somewhere else I can access the rootViewController on app launch to set additional steps up once?

There isn't a keyWindow at that time in the application lifecycle, so the reason there's no rootViewController to get on the keyWindow is because keyWindow is nil. But the app delegate has a property for your window, so you can just get self.window.rootViewController.
However, if you always have the same root view controller, you could probably do at least some of what you want to do (maybe all of it) in your root view controller's viewDidLoad method. This generally should only ever be called once, because with iOS 6 and later your offscreen view controllers' views are never unloaded.

You can get it from window:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
UIViewController *rootViewController = self.window.rootViewController;
return YES;
}

Related

How to change root controller after a server call on App Delegate?

I am trying to change root controller on app delegate.
I am making a server call to check if login session is valid or not. If valid, show welcome page, else show login page.
In the appDelegate , I am making server call in the method didFinishLaunchingWithOptions.
I get response through my datamodel delegate, but by the time I get response from server, the old root controller is already presented on screen.
At present, old root controller is presented first and within a fraction of seconds, new root controller is presented! So it looks like a flickering thing!
Is there a way to hold the old root controller to present untill I get repsonse from server ?
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
_dataModel=[DataModal sharedInstance];
_dataModel.delegate=self;
NSString *token=[[NSUserDefaults standardUserDefaults]objectForKey:#"token"];
if (!token) {
token=#"jkfhgjkfg908";
}else{
_dataModel.auth_token=token;
}
[_dataModel checkToken:token];
return YES;
}
//By the time it reaches here, the old root controller is already present on screen
-(void)checkToken:(NSDictionary *)items{
if ([items[#"success"] isEqual:#1]) {
AppDelegate *app = (AppDelegate*)[[UIApplication sharedApplication] delegate];
UIStoryboard *storyboard=[UIStoryboard storyboardWithName:#"Main" bundle:nil];
app.window.rootViewController=[storyboard instantiateViewControllerWithIdentifier:#"DashboardNavController"];
}else{
//do nothing, defualt is login view
}
}
You should not be waiting on a server call to decide on your root view controller - once didFinishLaunchingWithOptions is called you need to be presenting a view.
Have one Root View Controller present an initial view controller on load and stick to it - it's root for a reason.
If you have a token cached, skip your loginViewController and segue on. Plan for no internet/poor connection/ expired tokens, but don't put asynchronous actions in app delegate that prevent your UI being presented.
Uncheck Is initial View Controller checkbox for a controller in Main.storyboard
Initialize appDelegate's window property: appDelegate.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
Set rootViewController: appDelegate.window.rootViewController = controller;
And call makeKeyAndVisible method: [appDelegate.window makeKeyAndVisible];
Rather than changing rootViewController, using initialViewController as an entrance is a better approach in my opinion. When app is launched there must be a screen to make the user feel everything is going well. Also, you are making a request to server and this process may fail or timed out.
My advice is to make navigation through initialViewController. Make it visible and put some animation or activity indicator and then when the response come, if logged in, go to your apps mainViewController, else go to loginViewController. Also you can check internet connection here and advice user to connect to internet.
You cannot hold the old view controller to present, but instead you can present a view controller with black screen and then after you got the response form the server about the session you could present the view controller based on your needs.

Choosing what View to load when App goes back to foreground

How can I choose which ViewController my App should load when the app goes back to foreground?
Is it by default the last view used in the App?
Is there a way to choose a different view or viewcontroller? If so
how?
Maybe you could use:
Apple Documentation-UIAppDelegate-applicationWillEnterForeground
to change your window rootViewController property :)
So basically, you could instantiate your view controller in this method(from storyboard, or all in code), and do the following:
self.window.rootViewController = yourVc
By default, your last view is the one shown by your application.
If you want to change it to another view (for example a lock screen view), you must change your window.rootViewController inside your ApplicationDelegate code, inside both
- (void)applicationWillResignActive:(UIApplication *)application
and
- (void)applicationDidEnterBackground:(UIApplication *)application

How to reload rootViewController from appDelegate

I have a UINavigationController and on it I load a rootView that controlls the login process to my application.
I have recently added some code to my application delegate that checks my settings bundle for a logout request when this logout request happens I would like to either reload the rootView so that it loads the login hud, or just call the method inside rootView that shows the login hud.
This is how I set up the rootView for the navigationController inside my appdelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.window.rootViewController = self.navigationController; //Adds RootViewController to the NavigationController interface
// etc
}
What I would like to know is there a way to reload rootViewController? or call a method from it inside the application delegate?
It can be done but it's complicated. Best to avoid it if possible, and the specific requirements will be different for every app. Without seeing the source code for your app we can't tell you how it's done.
The basic process is you need to remove all of them from the view and set all references to nil, and then re-create it either from code or by loading the nib again.
A far better option is to leave the rootViewController where it is, and present a modal login view controller over the top of it. Once the user has logged in, send an NSNotification that the root view controller can observe, and populate its data.
Wait until after the notification has been sent to hide the login controller, and consider having the root view controller block the main thread while it performs any network operations pertaining to logging in. This way the login view (with a "logging in..." message?) will remain visible until the root view is fully populated.

In an AppDelegate, how is the main UIWindow instantiated?

A snippet of the default code in a Master-Detail Xcode project
AppDelegate.m
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// Override point for customization after application launch.
UINavigationController *navigationController = (UINavigationController *)self.window.rootViewController; // *** here ***
MasterViewController *controller = (MasterViewController *)navigationController.topViewController;
controller.managedObjectContext = self.managedObjectContext;
return YES;
}
AppDelegate.h
#property (strong, nonatomic) UIWindow *window;
I am aware that #synthesize just sets the accessor methods, and no initialization happens automagically. But how does window have a non-nil rootViewController if it is never explicitly initialized? Is this just Xcode init'ing behind the scenes?
From my book:
If you choose the Storyboard option as you specify a template, the process works a little differently. The app is given a main storyboard, pointed to by the Info.plist key “Main storyboard file base name” (UIMainStoryboardFile). After UIApplicationMain instantiates the app delegate class, it asks the app delegate for the value of its window property; if that value is nil, the window is created and assigned to the app delegate’s window property. The storyboard’s initial view controller is then instantiated and assigned to the window’s rootViewController property, with the result that its view is placed in the window as its root view; the window is then sent the makeKeyAndVisible message. All of that is done behind the scenes by UIApplicationMain, with no visible code whatever. That is why, in a storyboard template, the application:didFinishLaunchingWithOptions: implementation is empty.
From the UIWindow documentation:
Note: When you use storyboards and the Xcode app templates to create an app, a window is created for you.
If you don't use storyboards, the window is explicitly created, though all the standard project templates do this out of the box. You'll see a line similar to this in the app delegate:
self.window = [[[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]] autorelease];
Using storyboards, the window is created behind the scenes when the main storyboard is loaded (see the View Controller Programming Guide for more info).
From Apple's docs (in "Using View Controllers in Your App"):
The Main Storyboard Initializes Your App’s User Interface
The main storyboard is defined in the app’s Information property list file. If a main storyboard is declared in this file, then when your app launches, iOS performs the following steps:
It instantiates a window for you.
It loads the main storyboard and instantiates its initial view controller.
It assigns the new view controller to the window’s rootViewController property and then makes the window visible on the screen.
The above answers only who sets the window variable without answering the main questions: "But how does window have a non-nil rootViewController if it is never explicitly initialized? Is this just Xcode init'ing behind the scenes?" and seem to suggest that there is magic afoot. Not a satisfactory answer for me, and so with a little digging, all becomes clear.
The generated code defines AppDelegate as
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
...
}
When you search the project, there is no other reference to window, so apparently it should remain nil, but actually is set to the correct value (by the methods outlined above). The "magic" is that AppDelegate conforms to the UIApplicationDelegate which includes an declaration:
optional public var window: UIWindow? { get set }
Part of conforming to the UIApplicationDelegate is the redeclaration of the public variable window. When the underlying Application references the variable window in the protocol, it is actually linked to the variable window in our class. When the calling Application updates that variable window in the protocol, it is actually updating our variable window. So when we need to access the value in our program it is ready and waiting.
This is not Xcode magic, but an integral part of the Swift language. When using protocols we can employ the same techniques in our own Swift programs. This is just the same as our implementations of various functions in our classes which we do all the time: e.g. UIApplicationDelegate defines
optional public func applicationDidEnterBackground(_ application: UIApplication)
so we can write our own implementation which is then "magically" called!
For completeness, note the #UIApplicationMain tag on the class. This defines the entry point for the application and is what makes everything work together. The actual class name is irrelevant, and can be given any name you require, as long as it is of type UIResponder and conforms to the UIApplicationDelegate.
In your Storyboard, there is a little arrow you can drag around:
If you were using xibs/nibs instead, the 'Main Interface' field would be filled out.
In the end, yep, it's iOS/Xcode magic.

UISplitViewController not showing popup button when launching portrait

I am doing an iPad app based on a UISplitViewController. I have a little problem with the toobar button when my app launched in potrait. The button to show the popover is not displayed.
However when I rotate my iPad into landscape and then back to portrait, the button shows !
It looks like the following method is not called on launch (this is were I have the code showing the button):
- (void)splitViewController:(UISplitViewController *)svc willHideViewController:(UIViewController *)aViewController withBarButtonItem:(UIBarButtonItem *)barButtonItem forPopoverController: (UIPopoverController *)pc
This method is not called when the app launches but only when there is a rotation. What is even stranger is that I made a test app using Xcode UISplitViewController template + core data (which is similar to the app I am working on, and is the template I used to make this app). On the test app on which I have not made a single line of code, the button shows when I launch my app in portrait mode and the method above is also called upon launching, as opposed to my other app. Does anyone had a similar problem ?
Finally, it is not very clear from apple documentation whether this method is supposed to be called when a UISplitViewController is first shown:
http://developer.apple.com/library/ios/#documentation/uikit/reference/UISplitViewControllerDelegate_protocol/Reference/Reference.html%23//apple_ref/doc/uid/TP40009454
"Kshitiz" has the right concept. first I set the self.splitviewController.delegate = self in the viewDidLoad method, which it is a bit late to set this delegation. So, I tried to set the delegation in earlier stage which is awakeFromNib method. Then it works well.
So, the problem is after view already loaded by viewDidLoad, then the delegation will not work, it will work some time after some activities (such as rotate the iPad). So the earlier stage than viewDidLoad is awakeFromNib.
Here is the code that works:
- (void) awakeFromNib{
[super awakeFromNib];
self.splitViewController.delegate = self;
}
Have you set a splitviewcontroller delegate?
Generally the problem arises when delegate is not set.
I was having the exact same problem, and Martin Gunnarsson's response led me to the solution.
Before, I was setting the UISplitViewController's delegate property after the delegate view (the detail view) had already been loaded, in viewDidLoad:. By this time, the UISplitViewController had already sent the initial splitViewController:willHideViewController:withBarButtonItem:forPopoverController: message. I simply hadn't set the delegate soon enough.
The solution was to assign the delegate in the main app delegate, in application:DidFinishLaunchingWithOptions:. In this case, my delegate was contained within a navigation controller, so I had to dig one layer deeper to get it.
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
UISplitViewController *splitViewController = (UISplitViewController *)self.window.rootViewController;
UINavigationController *mainNavigationController = (UINavigationController *)[splitViewController.viewControllers objectAtIndex:1];
HPMainViewController *mainViewController = [mainNavigationController.viewControllers objectAtIndex:0];
splitViewController.delegate = mainViewController;
return YES;
}
This drove me spare as well, the more so since I'm working on two iPad projects with out-of-the-box splitViewController and the first one always shows the 'Master' button while the second one never did. I compared outlets and relationships and delegates until I was cross-eyed, but finally found the answer in the appDelegate. It turned out I had commented out a bit too much in the application:didFinishLaunchingWithOptions:, specifically where the splitViewController.delegate is set.
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// Override point for customization after application launch.
if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) {
UISplitViewController *splitViewController = (UISplitViewController *)self.window.rootViewController;
UINavigationController *navigationController = [splitViewController.viewControllers lastObject];
splitViewController.delegate = (id)navigationController.topViewController;
}
Adding this to the appDelegate saves you from having to subclass the splitViewController.
I also tried connecting the delegate in IB, but for some reason it wouldn't have none of that. Storyboard design flaw, imho.
I was stuck on this for quite sometime. Finally got it to work. The awakeFromNib did not work for me. The didFinishLaunchingWithOptions did. Might be because I am running some query that populates the items in the popover controller.
I'm having the same issue. My view is set up in IB, and it seems this is a timing issue. The split view delegate gets set after the split view has notified about the initial orientation "change". Adding the split view to an outlet in the app delegate made the button appear at portrait startup for me, but when I open the popup it's empty. This can probably be worked around somehow, but I think it's weird that the split view doesn't notify its delegate about the current orientation when it's set.

Resources