Changing content of UINavigationController's Stack result in crash - ios

I have a UINavigationController with 4 items:
(root)mainvc -> callerlistvc -> addcallerformvc -> verifycallervc (in that specific order)
When I am on the verifycallervc screen, if I press back, I want to go back to callerlistvc.
Here is the catch however, the back button should be a system button.. So.. as far as I know I cannot replace the action with a selector calling poptoviewcontroller:animated (only works on a custom uibarbuttonitem)
So then I thought of manipulating the stack (pretty interesting and challenging too!) So here is what I did...
So currently Im on the verifycallervc screen... and this gets called.
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
NSMutableArray *allViewControllers = [self.navigationController.viewControllers mutableCopy];
__block UIViewController *mainvc = nil;
__block UIViewController *callerlistvc = nil;
__block UIViewController *addcallerformvc = nil;
[allViewControllers enumerateObjectsUsingBlock:^(UIViewController *vc, NSUInteger idx, BOOL *stop) {
if ([vc isKindOfClass:[MainVC class]]) {
mainvc = vc;
} else if ([vc isKindOfClass:[CallerListVC class]]) {
callerlistvc = vc;
} else if ([vc isKindOfClass:[AddCallerFormVC class]]) {
addcallerformvc = vc;
}
}];
[self.navigationController setViewControllers:#[ mainvc, callerlistvc, self]];
}
After I did that, I pressed back normally and was now on the callerlistvc... great.
Unfortunately when I press the button (push-segued to addcallerformvc)... it results in a crash EXC_BAD_ACCESS.
I also tried a different approach by first manipulating the variable callerlistvc like so before adding it in the setViewControllers method
callerlistvc = [[UIStoryboard storyboardWithName:#"main" bundle:nil] instantiateViewControllerWithIdentifier:#"CallerListVC"];
But the result is the same.
I have added breakpoints and it goes like this...
CallerListVC:
tappedShowAddCallerListButton
performSegueWithIdentifier
prepareForSegue // identifier string is correct, destinationVC is not nil
then AddCallerFormVC:
4. viewDidLoad
5. viewWillAppear // properties not nil
after that EXC_BAD_ACCESS occurs
How can I make this work?

The better approach in this case would be to use a custom UINavigationController class and extend the popViewControllerAnimated: method. In this method either call the super method or pop to the specific view controller (super class method) based on a check. That way you have your system nav buttons and also control where the tack should pop to.

Don't override anything in verifycallervc and instead do following with normal back on verifycallervc
Override viewWillAppear or viewDidAppear for addcallerformvc like this
- (void)viewDidAppear:(BOOL)animated
{
if (![self isBeingPresented]) {
[self.navigationController popViewControllerAnimated:YES];
}
}
Reference :
https://developer.apple.com/library/ios/featuredarticles/ViewControllerPGforiPhoneOS/RespondingtoDisplay-Notifications/RespondingtoDisplay-Notifications.html#//apple_ref/doc/uid/TP40007457-CH12-SW7
Note: not tested, don't have XCode right now....

Related

Button not responding to selector

UPDATED FOR COMMENTS
I am trying to make this code work, but whenever I press the "nextButton", the program ends and I cannot figure out what went wrong. Can you please look at my code and figure out if anything is wrong.
- (void)viewDidLoad {
[super viewDidLoad];
Player1.placeholder = #"Player 1";
Player2.placeholder = #"Player 2";
Notes1.placeholder = #"Notes";
Notes2.placeholder = #"Notes";
[nextButton setAction:#selector(nextButtonPressed:)];
}
-(IBAction)nextButtonPressed:(id)sender {
if ([self.delegateP respondsToSelector: #selector(addPlayerViewController:didFinishEnteringPlayer1:didFinishEneteringPlayer2:)]) {
[self.delegateP addPlayerViewController:self didFinishEnteringPlayer1:Player1.text didFinishEneteringPlayer2:Player2.text];
NSLog(#"Works");
}
[self performSelector:#selector(nextButtonPressed:) withObject:nil afterDelay:0.25];
}
I also have another question to do with this code. To pass information through different view controllers using delegates, do you have to use a button or a bar button item?
The error that is displaying:
-[UIStoryboardPushSegueTemplate nextButtonPressed:]: unrecognized selector sent to instance
First you can´t put both lines:
[self presentModalViewController:gameDetails animated:YES];
[self.navigationController pushViewController: gameDetails animated:YES];
Second go to your viewController PlayersViewController and click in storyboard click on editor (top menú XCode) Embed In > Navigation Controller now you with your code change the next lines:
-(void) nextButtonPressed {
NSLog(#"Hi my friend");
[self performSegueWithIdentifier:#"YOUR_SEGUE_NAME_HERE" sender:#"hi"];}
And create new function:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{
// Make sure your segue name in storyboard is the same as this line
if ([[segue identifier] isEqualToString:#"YOUR_SEGUE_NAME_HERE"])
{
// Get reference to the destination view controller
YourViewController *vc = [segue destinationViewController];
// Pass any objects to the view controller here, like...
[vc setMyObjectHere:sender];
}}
The ViewController "YourViewController.h :
#interface YourViewController : UIViewController
#property (nonatomic, strong) NSString *MyObjectHere;
#end
Try setting a symbolic breakpoint on all Objective-C exceptions and see if that gives you your crash line numbers and error messages again.
The code you posted for setting up the target/action on your bar button item looks correct.
Edit: Disregard this part about changing the selector.
(I'm leaving it for continuity, but ignore it.)
One thing you might try is changing the selector to
:#selector(nextButtonPressed:) (Colon added)
And then adding the button as a parameter to your action method:
-(IBAction) nextButtonPressed: (UIButton *) sender
Action methods should work with or without the sender parameter, but
it's worth trying.
(Also you should add the IBAction keyword for clarity even if you're not hooking up the action in IB.)
EDIT: rmaddy pointed out that your code is trying to both modally present and push the same view controller. Don't do that. Do one thing or the other.
You should use either
[self presentModalViewController:gameDetails animated:YES];
(To present it as a modal)
or
[self.navigationController pushViewController: gameDetails animated:YES];
(To push it onto the navigation stack.
But not both. Doing both is almost certainly the cause of your crash.

iOS Get id of top most view controller in app delegate

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.

Check What Current rootViewController is?

I would like to check what the current rootViewController is.
I have a side menu viewController that slides out from the left of the screen and it displays 4 buttons - each point to a different viewController. When they are tapped, for example button1:
- (IBAction)button1Tapped:(id)sender {
[self.sideMenuViewController setContentViewController:[[UINavigationController alloc] initWithRootViewController:[[myViewController1 alloc] init]]
animated:YES];
[self.sideMenuViewController hideMenuViewController];
}
So I'm trying to do this:
User is on myViewContollerX and opens the sideMenuViewController.
On sideMenuViewController, buttonX is grey because the user was currently on myViewControllerX.
User taps buttonY and myViewControllerY shows.
On sideMenuViewController, buttonY is now grey because the user was currently on myViewControllerY.
So I'd need to check what the current rootViewController is, I assume. How would I do this? Thanks.
How to check your current rootViewController and use it in an if statement :
// Get your current rootViewController
NSLog(#"My current rootViewController is: %#", [[[UIApplication sharedApplication].delegate.window.rootViewController class]);
// Use in an if statement
UIViewController *rootViewController = [UIApplication sharedApplication].delegate.window.rootViewController;
if ([rootViewController isKindOfClass:[MyViewController class]])
{
NSLog(#"Your rootViewController is MyViewController!!");
}
Not sure which side-panel library you're using, but perhaps you can just do the styling of the buttons when they're tapped. Like this:
- (IBAction)button1Tapped:(UIButton *)sender
{
// .... set the center controller
[self setButtonAsActive:sender];
}
- (void)setButtonAsActive:(UIButton *)activeButton
{
for (UIButton *button in #[self.button1, self.button3, self.button3])
{
if (button == activeButton)
// ... make it highlighted
else
// ... make it not highlighted
}
}
You are using a setter to set contentViewController. Simply use a getter and read it.
e.g.
UIVIewController *contentViewController = self.sideMenuViewController.contentViewController;
NSLog(#"ContentViewController class: %#", [contentViewController class]);
you can check its class using:
if([contentViewController isKindOfClass: [UINavigationController class]])
{
// check nav root and disable button
}
Note:
It sounds like it would be much easier to just disable the button when its clicked and enable all the other buttons, but I'm not sure if you need this info for another reason as well.

PopViewController strange behaviour

Due to a weird request which I tried to turn down but it didn't work, I had to override the navigationBar's back Button.
I have made a custom UINavigationController subclass and hacked the
- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item method.
Here is my code:
#interface CustomUINavigationController ()
#end
#implementation CustomUINavigationController
#pragma mark - UINavigationBar delegate methods
- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item {
if ([[self.viewControllers lastObject] isKindOfClass:[ViewController1 class]]) {
ViewController1 *vc1 = (ViewController1 *)[self.viewControllers lastObject];
[vc1 handleBackAction];
if (vc1.canPopVC == YES) {
[self popViewControllerAnimated:YES];
return YES;
} else {
return NO;
}
}
[self popViewControllerAnimated:YES];
return YES;
}
#end
All works fine, except when I pop a viewController programmatically. The app crashed every time when I wanted to perform a push after said pop. Turning NSZombie on, revealed that when popping a viewController programmatically, its parent viewController is deallocated.
At this point, making a custom backButton is not a option since it will lose the native iOS 7 swipe to popViewController feature.
Crash log:
*** -[ContactsDetailViewController performSelector:withObject:withObject:]: message sent to deallocated instance 0x1806b790
(My previous post was completely wrong. This is a complete rewrite with an appropriate solution.)
I had this behavior pop up when I chose to delete some code generating a warning when I was converting to ARC -- code that I thought was not being called.
Here's the situation:
If you shadow navigationBar:shouldPopItem: in a subclass of UINavigationController, then the current view controller will NOT be popped when the user touches the NavBar's BACK button. However, if you call popViewControllerAnimated: directly, your navigationBar:shouldPopItem: will still be called, and the view controller will pop.
Here's why the view controller fails to pop when the user touches the BACK button:
UINavigationController has a hidden method called navigationBar:shouldPopItem:. This method IS called when the user clicks the BACK button, and it is the method that normally calls popViewControllerAnimated: when the user touches the BACK button.
When you shadow navigationBar:shouldPopItem:, the super class' implementation is not called, and hence the ViewController is not popped.
Why you should NOT call popViewControllerAnimated: within your subclass' navigationBar:shouldPopItem::
If you call popViewControllerAnimated: within navigationBar:shouldPopItem:, you will see the behavior that you desire when you click the BACK button on the NavBar: You can determine whether or not you want to pop, and your view controller pops if you want it to.
But, if you call popViewControllerAnimated: directly, you will end up popping two view controllers: One from your direct call to popViewControllerAnimated:, and one from the call you added to within navigationBar:shouldPopItem:.
What I believe to be the safe solution:
Your custom nav controller should be declared like this:
#interface CustomNavigationController : UINavigationController <UINavigationBarDelegate>
{
// .. any ivars you want
}
#end
Your implementation should contain code that looks something like this:
// Required to prevent a warning for the call [super navigationBar:navigationBar shouldPopItem:item]
#interface UINavigationController () <UINavigationBarDelegate>
#end
#implementation CustomNavigationController
- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item
{
BOOL rv = TRUE;
if ( /* some condition to determine should NOT pop */ )
{
// we won't pop
rv = FALSE;
// extra code you might want to execute ...
} else
{
// It's not documented that the super implements this method, so we're being safe
if ([[CustomNavigationController superclass]
instancesRespondToSelector:#selector(navigationBar:shouldPopItem:)])
{
// Allow the super class to do its thing, which includes popping the view controller
rv = [super navigationBar:navigationBar shouldPopItem:item];
}
}
return rv;
}
I'm not 100% certain but I don't think you should actually be popping the view controller in that delegate method.
"should" delegate methods don't normally do something. They just assert whether something should or shouldn't be done.
Change your method to this...
- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item {
if ([[self.viewControllers lastObject] isKindOfClass:[ViewController1 class]]) {
ViewController1 *vc1 = (ViewController1 *)[self.viewControllers lastObject];
[vc1 handleBackAction];
if (vc1.canPopVC == YES) {
return YES;
} else {
return NO;
}
}
return YES;
}
And see if it works.
All I have done is removed the popViewController calls.
EDIT - How to add a custom back button
In a category on UIBarButtonItem...
+ (UIBarButtonItem *)customBackButtonWithTarget:(id)target action:(#SEL)action
{
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
[button setBackgroundImage:[UIImage imageNamed:#"Some image"] forState:UIControlStateNormal];
[button setTitle:#"Some Title" forState:UIControlStateNormal];
[button addTarget:target action:action forControlEvents:UIControlEventTouchUpInside];
UIBarButtonItem *barButton = [[UIBarButtonItem alloc] initWithCustomView:button];
return barButtonItem;
}
Now whenever you want to set a custom back button just use...
UIBarButtonItem *backButton = [UIBarButtonItem customBackButtonWithTarget:self action:#selector(backButtonPressed)];
I would suggest a completely different approach.
Create a base class for the view controllers that you are pushing on the navigation stack. In the viewDidLoad method set your custom button as the leftBarButtonItem of the navigationItem and add a -backAction: which invokes the popViewControllerAnimated: method of the navigation controller.
That way you won't care about things like losing functionality of UINavigationController like the swipe to pop and you won't have to override the navigationBar:shouldPopItem: method at all.
You probably need to do [super shouldPop... instead of actual [self popViewControllerAnimated:YES];.
The reason being that the way UINavigationController implements stack is private, so you should mess with the method calls as little as possible.
Anyway, this looks like a hack. Moreover, the user will have no visual clue that you are blocking the navigation action. What's wrong with disabling the button via:
self.navigationController.navigationItem.backBarButtonItem.enabled = NO;
It's my fix to #henryaz answer for Xcode 11:
#interface UINavigationControllerAndNavigationBarDelegate : UINavigationController<UINavigationBarDelegate>
#end
#interface CustomNavigationController : UINavigationControllerAndNavigationBarDelegate
#end
// changed this method just a bit
- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item {
BOOL shouldPop = // detect if need to pop
if (shouldPop) {
shouldPop = [super navigationBar:navigationBar shouldPopItem:item]; // before my fix this code failed with compile error
}
return shouldPop;
}

UINavigationController is nil after pop

My project is complex (a bit too much probably) but the question is simple, here's the code
...
self.vc = [[ClassName alloc] init];
if ([[self.navigationController.viewControllers lastObject] isKindOfClass:[ClassName class]]) {
[self backAndGo:self.vc title:#"Title"];
}
else {
[self vai:self.vc title:#"title"];
}
...
in this code i've to go directly in the next view if the current view is different than ClassName, otherwise go back of 1 view and the go to the next.
- (void)backAndGo:(id)view title:(NSString *)title
- (void)backAndGo:(id)view title:(NSString *)title
{
NSLog(#"before %#,%d",[self.navigationController viewControllers],[[self.navigationController viewControllers] count]);
[self.navigationController popViewControllerAnimated:NO];
ALCParentViewController *viewController = (ALCParentViewController *)view;
[self.navigationController pushViewController:viewController animated:YES];
NSLog(#"after %#,%d",[self.navigationController viewControllers],[[self.navigationController viewControllers] count]);
}
ALCParentViewController is a parent class of vc, in this method the first log of navigatin controller is correct with all the stack of viewcontrollers, and the popViewController is executed but in the second log navigationController is null with 0 element obviously, and the pushViewController method is not executed, why? any ideas?
- (void)vai:(id)view title:(NSString *)title
- (void)vai:(id)view title:(NSString *)title
{
ALCParentViewController *viewController = (ALCParentViewController *)view;
[self.navigationController pushViewController:viewController animated:YES];
}
This is instead the other method to reach directly the second view, and it works correctly, another courius thing (almost for me) is that dispite the nil pushviewcontroller, navigation (push and pop) in other view works correctly...
EDIT 1:
As lucaslt89 said i've put a breakpoint in the second log and make "po self.navigationController" in the consolle, here the result
(lldb) po self.navigationController
$0 = 0x00000000 <nil>
(lldb)
it's nil, but i can see that in the log without breakpoint...
EDIT 2:
(lldb) po self.navigationController
$0 = 0x099787d0 <UINavigationController: 0x99787d0>
(lldb) po auxNavController
$1 = 0x099787d0 <UINavigationController: 0x99787d0>
(lldb)
after operations suggested by lucaslt89, the two address are the same
According to your last edit, one solution is to hold a reference to the navigation controller. Your backAndGo method should be like that
- (void)backAndGo:(id)view title:(NSString *)title
{
NSLog(#"before %#,%d",[self.navigationController viewControllers],[[self.navigationController viewControllers] count]);
UINavigationController *auxNavController = self.navigationController;
[self.navigationController popViewControllerAnimated:NO];
ALCParentViewController *viewController = (ALCParentViewController *)view;
[auxNavController pushViewController:viewController animated:YES];
NSLog(#"after %#,%d",[auxNavController viewControllers],[[auxNavController viewControllers] count]);
}
If you debug that, the address of self.navigationController prior to the pop should be the same as the auxNavController after the pop. Try that and tell us your results!
Ok, even when the question is closed, i'd like to make an explanation for future visitors. According to the documentation: ...This property is nil if the view controller is not embedded inside a navigation controller.
That's exactly what you do with the pop instruction. So, basically, before loosing a reference to the navigation controller because of the pop, make sure to store a reference to it.
On your backAndGo method:
First you pop a viewController, and then you push another ViewController.
The promblem is, after you pop a viewController, then the "self" is nil. So if you get the navigationController by using "self.navigationController" it will return nothing.
The reason is beacause "self" is nil, instead of the navigationController is empty. That is why the answer of lucastl89 is effective. He hold a navigationController object by a reference, not search it by using "self".

Resources