presentingViewController shows up as null - ios

I have three view controllers that are connected to a tab bar controller, which I thought should automatically set the presentingViewController/presentedViewController. However, when I toggle between my view controllers and I log [self presentingViewController], it logs null. I put the log in my viewDidAppear methods in each of the view controllers.
-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:YES];
NSLog(#" my presenting view controller is %#", [self presentingViewController]);
}
Output:
my presenting view controller is (null)

If you want to know which tab you came from, then you should subclass the tab bar controller, and set it as its own delegate. Create an integer property, oldIndex which you can set before the tab switch in the delegate method, tabBarController:shouldSelectViewController:.
#interface RDTBC () <UITabBarControllerDelegate>
#end
#implementation RDTBC
- (void)viewDidLoad {
[super viewDidLoad];
self.delegate = self;
}
- (BOOL)tabBarController:(UITabBarController *)tabBarController shouldSelectViewController:(UIViewController *)viewController {
self.oldIndex = self.selectedIndex;
return YES;
}
Then in your view controller, you can get it like this,
-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
NSLog(#"%ld",[(RDTBC *)self.tabBarController oldIndex]);
}

It's probably not a good idea to make one tab's behavior dependent on what tab was last shown. Tabs are designed to function independently.

Related

How can I prevent an unwind segue from UITabBar?

I have a Show segue that is embedded in a UITabBarController. I want to prevent an unwind segue when I tap the currently selected tab unless a certain condition is met. I've tried using shouldPerformSegueWithIdentifier and canPerformUnwindSegueAction but neither appear to be triggered when unwinding in this way.
Not sure what you mean by unwind segue on a tab bar, but if you want to prevent a tab change, there is a delegate function on UITabBarController for that purpose.
Add the protocol to your tab bar class.
#interface YourTabbarViewController () <UITabBarControllerDelegate>
#end
Assign the delegate, then later implement the function.
#implementation YourTabbarViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.delegate = self;
}
- (BOOL)tabBarController:(UITabBarController *)tabBarController shouldSelectViewController:(UIViewController *)viewController {
if (preventTabChange)
return NO;
return YES;
}
UPDATE
OK, assuming you have set up relevant parts as on this picture, and you want to prevent the unwind from B to A if certain conditions are met. My solution as described above will work.
As you will get a query/notification whenever the Navigation Controller is about to become active, you could create your own sub-class of that to hold whatever information you need to decide if it should be allowed to show or unwind from a sub-view controller. In that case your prevention could look like this (expanding the shouldSelectViewController above):
- (BOOL)tabBarController:(UITabBarController *)tabBarController shouldSelectViewController:(UIViewController *)viewController {
if ([viewController isKindOfClass:[YourNavigationController class]]) {
if ([(YourNavigationController *)viewController preventUnwind])
return NO;
}
return YES;
}
Note that I purposely chose preventUnwind as a flag in your custom class to say what to do. This will default to NO when you move to the view controller, and thus allowing that.
Don't forget to set YourTabbarViewController as the class for the Tabbar View Controller and YourNavigationController as the Navigation Controller in the picture.

Bug when swiping from view with uinavigationbar to one without

I have an application with 2 viewcontrollers, ViewController and NextViewController. ViewController does not have a navigation bar and has a white status bar. NextViewController does have a navigation bar and has a black (default) status bar. I have encountered a bug when swiping back to ViewController from NextViewController and cancelling the swipe where the navigation bar on NextViewController will disappear. The storyboard simply has the two views with a button and an action segue. The bug does not always happen, but often enough to be a problem. The bug has to do with the method - (UIStatusBarStyle) preferredStatusBarStyle, as everything works fine when I remove the methods.
Can I somehow stop this bug from happening while still having a white status bar on my ViewController and having the swipe enabled or am I forced to remove either functionality?
ViewController
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self setNeedsStatusBarAppearanceUpdate];
}
- (void) viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self.navigationController setNavigationBarHidden:YES animated:YES];
}
- (UIStatusBarStyle) preferredStatusBarStyle {
return UIStatusBarStyleLightContent;
}
NextViewController
#implementation NextViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self setNeedsStatusBarAppearanceUpdate];
}
- (void) viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self.navigationController setNavigationBarHidden:NO animated:YES];
}
-(UIStatusBarStyle)preferredStatusBarStyle{
return UIStatusBarStyleDefault;
}
Other possibly relevant info: Experiencing the issue on an iPhone 5s with iOS 8, but it also happens on the simulator with iOS 9. View controller-based status bar appearance is set to the default, YES.
Status bar style depends on - preferredStatusBarStyle of the view controller if the navigation bar is hidden, otherwise of the navigation controller, so the - preferredStatusBarStyle in your NextViewController is useless.
It seems that the bug is related with the status bar style of the navigation controller, you can subclass UINavigationController override the method childViewControllerForStatusBarStyle to avoid the bug.
#implementation MyNavigationController
- (UIViewController *)childViewControllerForStatusBarStyle {
return self.topViewController;
}
#end
Now the status bar style will depend on the view controller no matter whether the navaigation bar is hidden or not. And the bug would not happend.

Can i know in viewWillAppear that it was called after navigationController pop (back button)?

Say I have UIViewController A and B.
User navigates from A to B with a push segue.
Than user presses back button and comes to A.
Now viewWillAppear of A is called. Can I know in the code here that I came from back button (navigationController popTo...) and not by another way? And without writing special code in the B view controller.
hm, maybe you can use self.isMovingToParentViewController in viewWillAppear, see docs, if it is NO then it means the current view controller is already on the navigation stack.
I like to do the following in view controller A:
- (void) viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
if (_popping) {
_popping = false;
NSLog(#"BECAUSE OF POPPING");
} else {
NSLog(#"APPEARING ANOTHER WAY");
}
//keep stack size updated
_stackSize = self.navigationController.viewControllers.count;
....
}
- (void) viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
_popping = self.navigationController.viewControllers.count > _stackSize;
....
}
What you are doing is keeping track of whether your view controller (A) is disappearing because a view controller (B) is being pushed or for another reason. Then (if you did not modify the child view controller order) it should accurately tell you if (A) is appearing because of a pop on the navigation controller.
Add a BOOL property to UIViewController A:
#property (nonatomic) BOOL alreadyAppeared;
Then in your viewWillAppear: method, add:
if (!self.alreadyAppeared) {
self.alreadyAppeared = YES;
// Do here the stuff you wanted to do on first appear
}

iPad modal controller is dismissed after rotation

I am presenting a modal view using a storyboard segue set as Form Sheet.
The problem is, when I rotate the iPad after this view is displayed, the view is removed from the view/dismissed.
I have no idea why. It only seems to occur when starting in Portrait then rotating to Landscape.
If I start in Landscape then show the view then rotate it stays on the screen fine.
Any ideas?
EDIT ----
It also seems that full screen modal views are also dismissed after rotation!
There's nothing special going on in the presentation code, this is a full screen modal:
EditViewController *editView = [self.navigationController.storyboard instantiateViewControllerWithIdentifier:#"editViewController"];
editView.delegate = self;
editView.image = image;
editView.modalTransitionStyle = UIModalTransitionStyleFlipHorizontal;
[self presentViewController:editView animated:YES completion:nil];
This happens on both iOS 6 and iOS 7
EDIT 2 ----
Forgot to mention, i'm presenting the modal from the left/master view controller of a UISplitViewController
late, but what it worked for me was just before
[self presentViewController:aController animated:YES completion:nil];
dismiss the master controller, adding this lines
[self.splitViewController setPreferredDisplayMode:UISplitViewControllerDisplayModePrimaryHidden];
[self.splitViewController setPreferredDisplayMode:UISplitViewControllerDisplayModeAutomatic];
and then present your controller
Get rid of: editView.modalTransitionStyle = UIModalTransitionStyleFlipHorizontal;
That will solve the issue you are experiencing. Modal view controllers presented on iPad as a Form Sheet do not rotate correctly using that transition style.
its really hard to get the cause how and why this is happening as i found that this also happen with UIPopover also as when you rotate it UIPopover hide because ???
So if you want to keep your view then just call again your controller after rotation will do fine user experience
This is not a bug its a limitation on UISplitViewController. The problem exists when the masterViewController (which is a UIPopoverController) is able to be dismissed. Heres how it works with the assumption that your app does allow the masterViewController to be dismissed in portrait and does not allow in landscape.
In portrait while the masterViewController is visible, if you were to present a modal from a viewController in the masterViewController and then rotate to landscape, the modal would disappear in iOS7 and the app would not rotate in iOS8. iOS8 introduces a condition to prevent the bad experience of iOS7. iOS7 losses the modal in the process of moving the masterViewController from the popoverController to a contained viewController in the splitViewController.
The modal needs to be presented from the splitViewController and not from the masterViewController. The only problem with this is the modal gets presented below the masterViewController in portrait. My solution is to dismiss the masterViewController and then present the modal.
There are several ways to achieve this result depending on how complex your code needs to be. Here's how I do this in my app.
I first subclass UISplitViewController in order to have a reference to the popoverController. I use delegate forwarding in order to access the delegate methods internally and externally. Heres the .h
// MainSplitViewController.h
#import <UIKit/UIKit.h>
#interface MainSplitViewController : UISplitViewController
#property (nonatomic, weak, readonly) UIPopoverController* primaryColumnController;
#end
And the .m
// MainSplitViewController.m
#import "MainSplitViewController.h"
#interface MainSplitViewController () <UISplitViewControllerDelegate>
#property (nonatomic, weak) id<UISplitViewControllerDelegate> externalDelegate;
#property (nonatomic, weak) UIPopoverController* primaryColumnController;
#end
#implementation MainSplitViewController
- (instancetype)init {
self = [super init];
if (self) {
self.delegate = self;
}
return self;
}
#pragma mark - Split View Controller Delegate
- (void)splitViewController:(UISplitViewController *)svc popoverController:(UIPopoverController *)pc willPresentViewController:(UIViewController *)aViewController {
self.primaryColumnController = pc;
if ([(id)self.externalDelegate respondsToSelector:_cmd]) {
[self.externalDelegate splitViewController:svc popoverController:pc willPresentViewController:aViewController];
}
}
#pragma mark - Delegate Forwarder
- (void)setDelegate:(id<UISplitViewControllerDelegate>)delegate {
[super setDelegate:nil];
self.externalDelegate = (delegate != self) ? delegate : nil;
[super setDelegate:delegate ? self : nil];
}
- (BOOL)respondsToSelector:(SEL)aSelector {
id delegate = self.externalDelegate;
return [super respondsToSelector:aSelector] || [delegate respondsToSelector:aSelector];
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
id delegate = self.externalDelegate;
return [delegate respondsToSelector:aSelector] ? delegate : [super forwardingTargetForSelector:aSelector];
}
#end
Next I create a class extension on UIViewController
// UIViewController+Popover.h
#import <UIKit/UIKit.h>
#interface UIViewController (Popover)
- (UIViewController *)popoverPresentingViewController;
#end
And the .m
// UIViewController+Popover.m
#import "UIViewController+Popover.h"
#import "MainSplitViewController.h"
#implementation UIViewController (Popover)
- (UIViewController *)popoverPresentingViewController {
UIViewController* viewController = self;
if ([self.splitViewController isKindOfClass:[MainSplitViewController class]]) {
viewController = self.splitViewController;
MainSplitViewController* mainSplitViewController = (MainSplitViewController *)self.splitViewController;
if (mainSplitViewController.primaryColumnController.popoverVisible) {
[mainSplitViewController.primaryColumnController dismissPopoverAnimated:YES];
}
}
return viewController;
}
#end
Now where ever you present the modal, instead of calling [self presentViewController: ... call [self.popoverPresentingViewController presentViewController: ...]. Remember to import UIViewController+Popover.h
your question came closest to my bug, On returning from modalView the parentView will switch to orientation in which the application was opened.
Visually it appears that the modal view is rotated and then returns.
I solved it by removing the modal view altogether, and using
[self.navigationController pushViewController: <the View(not modal now)>]
instead of using-
[self presentViewController:<Modal View>]
I think this is because the navigation controller doesn't own the Modal View, hence it reloads - when returning from the modal view - to incorrect orientation
Problem:
When presenting a view controller modally, it gets dismissed on rotation.
Approach:
Set the UISplitViewControllerDelegate
Use the UISplitViewControllerDelegate methods
Hold a reference to your modal view controller in an instance variable
Check if your modal view controller's presenting view controller exists.
If it exists, nothing needs to be done, else just present without any animation.
UISplitViewControllerDelegate methods:
func primaryViewController(forCollapsing splitViewController: UISplitViewController) -> UIViewController? {
if let someModalViewController = someModalViewController,
someModalViewController.presentingViewController == nil {
let masterViewController = viewControllers.first
masterViewController?.present(someModalViewController,
animated: false) {
}
}
return nil
}
func primaryViewController(forExpanding splitViewController: UISplitViewController) -> UIViewController? {
if let someModalViewController = someModalViewController,
someModalViewController.presentingViewController == nil {
let masterViewController = viewControllers.first
masterViewController?.present(someModalViewController,
animated: false) {
}
}
return nil
}
Note:
UISplitViewControllerDelegate has quite a methods, it can be daunting initially, if you spend some time experimenting, you can achieve what you want.
It has fine grained access.
I'm very late but try this. It works for me.
[self.splitViewController presentViewController:editView animated:YES completion:nil];

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;
}

Resources