When my app resumes I find the top most view controller (whatever was last presented when the app was active) in one of my app delegate methods. I know it works because calling viewDidLoad runs the one from the right view controller.
However I only want to call viewDidLoad if the top most controller is one of a particular few view controllers. How do I do this? I've attempted returning the restoration id, title and navigationitem.title of the view controller to find which one it is but all return (null). I set the restoration id programmatically before I call any view controller so it should be ok, but somehow is not when accessed in the app delegate.
Edit:
Process is as follows:
The first time the app starts the app delegate method didFinishLaunchingWithOptions checks if some data is stored. If it is, the following code is run:
AACMainViewController *firstController = [storyBoard instantiateViewControllerWithIdentifier:#"aacmainViewController"];
firstController.restorationIdentifier = #"AACMainViewController";
UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:firstController];
self.window.rootViewController = navController;
The user is now in the main menu. If at this point the user shrinks the app and then resumes it, the app delegate method applicationDidBecomeActive is eventually called. When it is, it runs another method. The code we're interested in is this:
UIViewController * top = [self topViewController];
//[top viewDidAppear:YES];
NSString * toptitle = top.title;
NSString * resid = top.restorationIdentifier;
NSLog(#"App top res ID is %#",resid);
NSLog(#"App top title is %#",top title);
The method topViewController and the method it calls are:
- (UIViewController *)topViewController
{
return [self topViewController:[UIApplication sharedApplication].keyWindow.rootViewController];
}
- (UIViewController *)topViewController:(UIViewController *)rootViewController
{
NSLog(#"%#",#"topViewController: ---");
if (rootViewController.presentedViewController == nil)
{
NSLog(#"%#",#"presentedViewController == nil");
return rootViewController;
}
//isMemberOfClass: //isKindOfClass:
if ([rootViewController.presentedViewController isMemberOfClass:[UINavigationController class]])
{
NSLog(#"%#",#"isMemberOfClass");
UINavigationController *navigationController = (UINavigationController *)rootViewController.presentedViewController;
UIViewController *lastViewController = [[navigationController viewControllers] lastObject];
return [self topViewController:lastViewController];
}
NSLog(#"%#",#"End");
UIViewController *presentedViewController = (UIViewController *)rootViewController.presentedViewController;
return [self topViewController:presentedViewController];
}
As it stands, although a title is shown in the Main view when you are running the app, the NSLog in the app delegate will think it is null!
Edit 2:
I found that if you manually set the title in viewDidLoad or viewDidAppear BEFORE you shrink it, then it knows about the title on resume. Which doesn't make sense since the title is already shown on the device. It also presents a problem. Once you set the title in code this way (let's say title is "Test1"), it doesn't matter which view you shrink and resume from, unless you set the title in code for that new view controller (say "Test2"), it'll still think the title is the last one ("Test1"). I've a lot of view controllers so manually setting it is too tedious to contemplate.
Related
I have a UIViewController (which is my apps root view controller) that contains a UINavigationController that I reload using
- (void)presentSelectedViewController
{
[[self navController] popToRootViewControllerAnimated:NO];
[[self navController] setViewControllers:#[[self selectedViewController]] animated:NO];
}
self.selectedViewController is set through this:
- (void)setSelectedViewController:(UIViewController *)selectedViewController
{
if (_selectedViewController != selectedViewController)
{
if ([selectedViewController isKindOfClass:[SNPStandInViewController class]])
{
Class newClass = [(SNPStandInViewController *)selectedViewController actualClass];
if ([newClass class] == [SVWebViewController class])
selectedViewController = [[[(SNPStandInViewController *)selectedViewController actualClass] alloc] initWithAddress:[PREFERENCES urlForDefaultSearchProvider]];
else
selectedViewController = [[[(SNPStandInViewController *)selectedViewController actualClass] alloc] init];
if ([selectedViewController isKindOfClass:[SNPSettingsViewController class]])
[(SNPSettingsViewController *)selectedViewController setDelegate:[self rootController]];
}
Class oldClass = [self.selectedViewController class];
NSInteger oldControllerIndex = [self.viewControllers indexOfObject:self.selectedViewController];
[self.selectedViewController willMoveToParentViewController:nil];
[self.selectedViewController removeFromParentViewController];
[(SNPStandardViewController *) self.selectedViewController releaseResources];
[self willChangeValueForKey:#"selectedViewController"];
_selectedViewController = selectedViewController;
[self didChangeValueForKey:#"selectedViewController"];
[self.viewControllers replaceObjectAtIndex:_selectedIndex withObject:selectedViewController];
if (nil != oldClass && oldControllerIndex != NSNotFound)
{
[self.viewControllers replaceObjectAtIndex:oldControllerIndex withObject:[[SNPStandInViewController alloc] initWithClass:oldClass]];
}
[[self indexTableView] reloadData];
}
[self presentSelectedViewController];
}
(The standin view controller class is an NSObject subclass that basically holds the class of the view controller that should be there and creates it if selected -- though it is visually not a tab interface, think a tab interface where I don't want to have the actual view controllers that are not selected be created and in memory and running)
I have one view controller that has a UICollectionView in it and this is the default view controller that is set using the above code when the app first runs. It runs fine and the UICollectionView loads its data before the -(void)viewWillAppear: of that view controller runs (a -(void)loadView implementation creates the UICollectionView and does a [uiCollectionViewInstance reloadData]).
If I select another view controller above, and then reselect the initial one (which is a new instance -- old instances when moved off of are destroyed), the UICollectionView does not load its data (even though the loadView routine creates it and calls reloadData) until after both viewWillAppear: and viewDidAppear actually run. This is bad because I call a selection routine in viewWillAppear: to set a highlight in the collection view on the item that is the default selection [think a button bar type interface].
The same code path seems to run each time I create this view controller that contains the collection view (with extensive NSLog outputting to show the path). So I assume that maybe something is happening in the actual routine that creates the new view controller and sets it as the active one in the UINavigationController
I am at a loss to know what to explore to see why this collection view has no visible cells after being created and reload data being called on it when it is created after being selected in my pseudo tab controller described above (but it works fine when set initially upon creation of the pseudo tab controller).
I don't know what else people want to see but I'd be happy to try and post more info, code, etc as necessary.
in the -presentSelectedViewController I added this after doing the setViewControllers and now it all works fine.
[[[self selectedViewController] view] setNeedsLayout];
[[[self selectedViewController] view] layoutIfNeeded];
I have a HomeView and a HomeDropDownView.
HomeDropDownView is shown as a drop-down view over the HomeView.
HomeView is a delegate of HomeDropDownView.
When I do an action in HomeDropDownView I want to call a delegate method in HomeView and have that delegate method present a third view controller, TestViewController from it's navigation controller.
If I try to launch TestViewController from anywhere in the class it works fine - except from the delegate method.
There are animations in HomeDropDownView but putting the call to the delegate method in the complition does not make the view controller appear. And in the case that I'm using this the animation's don't fire anyway; there's only a resizing without animation.
TestViewController's init does get called as well as the viewDidLoad but not the viewWillAppear and the view dose not appear.
Code:
HomeDropDownView
- (void)finalAction {
...
[self callDelegateAction];
...
- (void)calldelegateAction {
if ([self.delegate respondsToSelector:#selector(launchTestView)] ) {
[self.delegate launchTestView];
} else {
DLog(#"Error out to the user.");
}
}
HomeView
- (void)launchTestView {
//[self listSubviewsOfView:self.parentViewController.view];
NSLog(#"delegate method | self: %#", self);
TestViewController *tvc = [[TestViewController alloc] initWithNibName:#"TestViewController" bundle:nil];
//[self.navigationController presentViewController:tvc animated:YES completion:nil];
//[self.view.window.rootViewController presentViewController:tvc animated:YES completion:nil];
//[self.navigationController pushViewController:tvc animated:YES];
AppDelegate *appdelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
[appdelegate.tabBarController.navigationController presentViewController:tvc animated:YES completion:^() {
NSLog(#"Done!");
}];
}
None of the above approaches work. But if I put the exact same code into the viewDidAppear or put it in a button action method, it will work fine. At the time of calling the delegate method's self is HomeView and all the subviews, including the nav controller do seem to be there. This is in a tabcontroller-based project but I think that any of the above are acceptable ways to call the nav controller still.
What am I missing? Why does my delegate method not want to push/present a viewcontroller on HomeView's Nav controller? It's probably something I'm missing but I can't find a reason in the Apple Docs or any other thread.
Thanks for the help!
Sadly this turned out to be that HomeView was being changed underneath the execution of the message. So by the time the HomeView got the message call it was no longer the same HomeView object that had requested action in the first place. So it was not the same delegate.
This was done so that it would appear to the user that the same view was being used for different things.
But this is a good example of why you should not destroy and re-create critical views. We should have been using the same view and reloading the objects instead if we knew that we would be sending messages. Or had some notion of a control structure.
I have a PIN view controller which needs to be presented every time the - (void)applicationWillEnterForeground:(UIApplication *)application fires. I have multiple view controllers and when the app enters background the user could be on any of them.
The problem is I don't know how to present the PIN view controller over any view controller that is currently active. Here's how my implementation looks:
- (void)applicationWillEnterForeground:(UIApplication *)application
{
ResourceSingleton *resource = [ResourceSingleton sharedSingleton];
if ([resource checkIfPINIsEnabled])
{
PinViewController *pinView = [[PinViewController alloc] initWithMode:kPINViewControllerModeEnter];
pinView.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
[self.window.rootViewController presentViewController:pinView animated:YES completion:NULL];
}
}
But the PIN view appears only if I'm at the first view controller (the root one). How to pop it up on any view controller?
I have seen Using applicationwillenterforeground for a passcode screen but there has to be a better way or am I wrong? This will be for iOS 7 so if only 7 has such a functionality its ok but I am pretty sure it can be done on 6 as well.
If your root view controller is a NavigationController, then pushing or presenting should work in most cases. You already have all the code in place, just create a navigation controller. The only case this would not work is if there is a modal view controller already presented. In that case that needs to be dismissed first.
Here is a little messy implementation that takes care of this case too.
AKPresentedViewController *pres = [self.window.rootViewController.storyboard instantiateViewControllerWithIdentifier:#"pres"];
UINavigationController *navi = ((UINavigationController*)self.window.rootViewController);
if (navi.presentedViewController) {
[navi.presentedViewController dismissViewControllerAnimated:YES completion:^{
[navi presentViewController:pres animated:NO completion:nil];
}];
} else {
[navi presentViewController:pres animated:NO completion:nil];
}
You could have the app delegate handle the logic for the PIN view, and have that be a view, rather than a view controller. Just add the view as a subview of the window, and it will be shown over anything else.
- (void)applicationWillEnterForeground:(UIApplication *)application {
UINib *pinNib = [UINib nibWithNibName:#"PINView" bundle:nil];
UIView *pinView = [pinNib instantiateWithOwner:self options:nil][0];
[self.window addSubview:pinView];
}
If you make the app delegate the File's Owner of the xib, then you can hook up any outlets you need in the view to the app delegate.
You could present your PIN view controller the way they mention here.
And for popping the PIN view controller I'm guessing that the user has to enter the correct PIN so the PIN view controller goes away. In that case it can pop itself:
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
Hope this helps!
Convert to the objective-C codes to Swift 4, put the codes into func applicationWillEnterForeground
let pres = self.window?.rootViewController?.storyboard?.instantiateViewController(withIdentifier: "StartVC")
let navi: UINavigationController = self.window?.rootViewController as! UINavigationController
if ((navi.presentedViewController) != nil) {
navi.dismiss(animated: true) {
navi.present(pres!, animated: false, completion: nil)
}
} else {
navi.present(pres!, animated: false, completion: nil)
}
I am working on an app that at launch checks for valid login credentials, and if they are found and not expired the main split view controller is displayed, and if not a login screen should be displayed.
Each part is working fine separately, but I am struggling with the best way at launch time to select the proper view to display.
I have tried setting up a modal segue from the root view controller, and in my application:didFinishLaunchingWithOptions: function in the App Delegate, calling this:
// Segue to the login view controller...
if (loginNeeded) {
[self.window.rootViewController performSegueWithIdentifier:#"LoginScreen" sender:self];
}
This logically should work, but triggering segues from within the app delegate seems to be impossible.
What is the ideal place and technique for handling this?
You could try a custom segue, as per this post hiding-a-segue-on-login-process.
Alternatively if you're desperate to have the login display before the split view controller loads try something along the following lines...
Create your login screen on the main storyboard as, say, a subclass of UIViewController. Make sure it is the initial scene (check Is Initial View Controller).
On the storyboard, create a new segue from your login class to the original SplitViewController. Give it an identifier, 'Load SplitViewController' and a segue custom class name which we'll call FullyReplaceSegue.
In your login class .m file, add code to be called once the user has logged in:
[self performSegueWithIdentifier:#"Load SplitViewController" sender:self];
Create the new segue class, based on UIStoryboardSegue and name it FullyReplaceSegue as per above.
.h file
#import <UIKit/UIKit.h>
#interface : UIStoryboardSegue
#end
.m file
#import "FullyReplaceSegue.h"
#implementation FullyReplaceSegue
- (void)perform
{
UIViewController *dest = (UIViewController *) super.destinationViewController;
UIWindow *window = [UIApplication sharedApplication].keyWindow;
window.rootViewController = dest;
if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) {
UISplitViewController *splitViewController = (UISplitViewController *)dest; // assumes we're transitioning to a UISplitViewController!
UINavigationController *navigationController = [splitViewController.viewControllers lastObject];
splitViewController.delegate = (id)navigationController.topViewController;
}
}
#end
Here's how I did it.
In didFinishLaunchingWithOptions:
//save the root view controller
[[self window] makeKeyAndVisible];
UINavigationController *navigationController = (UINavigationController*) self.window.rootViewController;
rootController = [[navigationController viewControllers] objectAtIndex:0];
Somewhere else in the app delegate:
[rootController performSegueWithIdentifier:#"fileSegueID" sender:self];
Then, in the storyboard, create a segue from the view that gets assigned as "rootController", to the desired optional view, and give that new segue the id fileSegueID. It takes some debugging to make sure the rootController variable gets assigned to the correct view.
Maybe a little late, but I was looking for the same suggestions. Here's what I wound up doing.
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:#"Signup" bundle:nil];
if(isLoggedIn) {
UINavigationController *navigationController = (UINavigationController*) self.window.rootViewController;
IndexController *ivc = [storyboard instantiateViewControllerWithIdentifier:#"IndexController"];
[navigationController pushViewController:ivc animated:NO];
}
Why don't you load the screen that would be visible assuming proper and non-expired log-in credentials (by setting it as the root view controller of the window), and then in viewDidLoad of that first view controller, check if an update to the login credentials are needed. If so, segue into the login view controller.
Yes, it can be used, if you get a reference to the segue's parent view controller. You can get it like this:
UINavigationController *navigationController = (UINavigationController*) self.window.rootViewController;
[[[navigationController viewControllers] objectAtIndex:0] performSegueWithIdentifier:#"LoginScreen" sender:self];
This will only work if the index in viewControllers array matches the one of your view controller and if it exists of course. In this case is the first one (in the array and storyboard).
The segue ("LoginScreen") must not be attached to an action. The way you do this is by control-dragging from the file owner icon at the bottom of the storyboard scene to the destination scene. A popup will appear that will ask for an option in “Manual Segue”; pick “Push” as the type. Tap on the little square and make sure you’re in the Attributes Inspector. Give it an identifier which you will use to refer to it in code.
I'm obviously missing something...
In my iOS app, I push a UIViewController onto a navigation controller:
MyViewController *mvc = [[MyViewController alloc] initWithNibName:#"MyViewController"];
[self.navigationController mvc animated:YES];
MyViewController displays fine, and I can see and use the navigationBar, but when I try to get a pointer back to the navigation controller from within my view controller, I get a nil result.
UINavigationController *nav = [self navigationController];
if (!nav) {
NSLog(#"no nav");
}
I've been beating my head against this all day, but can't see that I'm doing anything wrong. I get no warnings or errors in Xcode. Am I completely missing something?
TIA: john
The navigationController won't be set properly on viewDidLoad. You have to check it in viewDidAppear or at some later stage. Which method are you calling to [self navigationController] in?
The reason for this is that when viewDidLoad is called, the UINavigationController is still processing the pushViewController:animated: method. It would appear to set the navigationController property after it initialises the controller's view. I can't recall whether the property is set by the time viewWillAppear runs, but it should definitely be set by viewDidAppear.
id rootViewController = [[[[[UIApplication sharedApplication] keyWindow] subviews] objectAtIndex:0] nextResponder];