Screen edge gesture when using UISplitViewController with hidden navigation bar - ios

In an iPhone app, we have the ability to hide the navigation bar. By default, calling [self.navigationController setNavigationBarHidden:YES animated:YES] to hide the navigation bar stops the screen edge gesture recognizer self.navigationController.interactivePopGestureRecognizer from working, but in the past I could restore this functionality by setting its delegate to my own (while the bar is hidden) that does this:
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
return YES;
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
return ![otherGestureRecognizer isKindOfClass:[UIScreenEdgePanGestureRecognizer class]];
}
However, this doesn't work now that the app is using UISplitViewController.
It's unclear if the split view controller is using its own gesture recognizer, or some other issue. I haven't been able to figure out how to make it work.
I can't find any other discussion on this. The most recent posts I've found are years old, e.g. this one, this one, or this one.
What am I missing? How can I get a screen edge interactive gesture to work with UISplitViewController when the navigation bar is hidden?

Thank you rubber duck. 🐥
Of course, soon after posting that, I figured out the answer: when in compact layout, e.g. on iPhone, the UISplitViewController has a UINavigationController to manage the hierarchy, plus the detail view controller has its own UINavigationController.
So the solution was to use the interactivePopGestureRecognizer of the outer navigation controller (that has multiple view controllers), not the inner one (with only one), thusly:
#objc var parentNavigationController: UINavigationController? {
return navigationController?.parent as? UINavigationController
}
I hope this helps someone else encountering this issue.

Related

Swipe gesture being added to a UIView or UIViewController upon segue

I'm completely stumped by this. I've been working on a project that has both a landscape view and portrait view. I segue from one view controller to another, and the 2nd view controller has a UIView that allows custom drawing. However, when I'm in landscape mode (and only when in landscape mode), I'm unable to draw in the view (with touches... functions) because when I'm in the left half of the view and I drag towards the left side of the screen, I get taken back to the previous view controller.
The view controllers are embedded in a navigation controller, so I'm not sure if this is some built-in segue that the navigation controller creates to send me back or something, but other than that there's nothing I can think of that'd be causing this.
Any idea what may be causing this? I've already checked the UIView itself and there are no gesture recognizers associated with it, so I don't know where it'd be hiding.
if ([self.navigationController respondsToSelector:#selector(interactivePopGestureRecognizer)]) {
self.navigationController.interactivePopGestureRecognizer.enabled = NO;
}
Add this to you viewDidLoad function of your second view controller.
iOS 7 and above has a feature to go back to previous view controller when swipe from the left end
UPDATE
self.navigationController.interactivePopGestureRecognizer.delegate = self
func gestureRecognizerShouldBegin(gestureRecognizer: UIGestureRecognizer!) -> Bool {
return false;
}

ios,How to preform (interactivePopGestureRecognizer )back action in Collection view

ListViewController-> DetailsViewController, the navigationController of the DetailsViewController support interactivePopGestureRecognizer feature, that can swipe right and back to ListViewContorller, it is fine,
just the DetailsViewController contains some UICollectionView, it does not response the swipe gesture, it is meaning if user touch the CollectionView swipe,drag the view from left to right, the navigationController not got the action at all, how to solve this problem ?
I just try this way:
[collectionView addGestureRecognizer: self.navigationController.interactivePopGestureRecognizer];
but it not works .
so then I create new 'Swipe Gesture Recognaizer' and bind to collectionView, also link to the selector action as bellow:
I add code in the details view:
-(IBAction)swipeBack:(id)sender
{
[self.navigationController popToRootViewControllerAnimated:true];
}
then, if user touch the collectionView, then it can back to list view controller, but it not is good enough, because it not works the same to 'interactivePopGestureRecognizer',
anyone know other best solution for this purpose ? thanks for your time.
Like it is mentioned in https://stackoverflow.com/a/18947952/1113407 multiple gesture work better if
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
return YES;
}
is set.
If that is not enough, try adopting https://stackoverflow.com/a/2628777/1113407 for your needs.
In UINavigationContoller swipe back function not working if its view is added to UIViewContoller "Mr.1" mentioned:
Updated: It turns out that if the navigation bar is hidden, the swipe function will be disabled....
Did you hide the navigation bar?

Navigation bar title bug with interactivePopGestureRecognizer

I am having a weird problem with UINavigationBar's title in an app when interactivePopGestureRecognizer comes into play. I have made a demo app to showcase this bug.
Setup:
The rootViewController is a UINavigationController.
FirstViewController has the navigation bar hidden, and interactivePopGestureRecognizer.enabled = NO;
Second and ThirdViewControllers have the navigation bar visible and the popgesture enabled.
Bug:
The bug occurs when going back from the Second to the First view using the popgesture. If you pull the second view halfway and then go back to the second view, the navigation title will show "Second View" (as expected).But when you go to the Third view, the title will not change to "Third View". And then on clicking the back button of the Third view, the navbar will get messed up.
Please check out my demo app. Any help explaining why this bug is happening will be appreciated. Thanks!
Remove Red Herrings
First of all, your example can be greatly simplified. You should delete all the viewDidLoad stuff, as it is a complete red herring and just complicates the issue. You should not be playing around with the pop gesture recognizer delegate on every change of view controller; and turning the pop gesture recognizer off and on is irrelevant to the example (it is on by default, and should just be left on for this example). So delete this kind of thing in all three view controllers:
- (void)viewDidLoad {
[super viewDidLoad];
if ([self.navigationController respondsToSelector:#selector(interactivePopGestureRecognizer)]) {
self.navigationController.interactivePopGestureRecognizer.enabled = NO;
self.navigationController.interactivePopGestureRecognizer.delegate = self;
}
}
(Don't delete the code that sets self.title, though you could have made things even simpler by doing that in the xib file for each view controller.)
You can also get rid of other unused methods throughout, such as the init... methods and memory alert methods.
Another issue, by the way, is that you have forgotten to call super in your implementations of viewWillAppear:. It is required that you do this. I don't think that affects the bug, but it is well to obey all the rules before you start trying to track these things down.
Now the bug still happens but we have much simpler code, so we can start to isolate the issue.
How The Pop Gesture Works
So what's the cause of the problem? I think the most obvious way to understand it is to realize how the pop gesture works. This is an interactive view controller transition animation. That's right - it's an animation. The way it works is that the pop animation (slide from the left) is attached to the superview layer, but with a speed of 0 so that it doesn't actually run. As the gesture proceeds, the timeOffset of the layer is constantly being updated, so that the corresponding "frame" of the animation appears. Thus it looks like you are dragging the view, but you are not; you are just making a gesture, and animation is proceeding at the same rate and to the same degree. I have explained this mechanism in this answer: https://stackoverflow.com/a/22677298/341994
Most important (pay attention to this part), if the gesture is abandoned in the middle (which it almost certainly will be), a decision is made as to whether the gesture is more than half-way completed, and based on this, either the animation is rapidly played to the end (i.e. the speed is set to something like 3) or the animation is run backwards to the start (i.e. the speed is set to something like -3).
Solutions And Why They Work
Now let's talk about the bug. There are two complications here that you've accidentally banged into:
As the pop animation and pop gesture begin, viewWillAppear: is called for the previous view controller even though the view may not ultimately appear (because this is an interactive gesture and the gesture may be cancelled). This can be a serious issue if you are used to the assumption that viewWillAppear: is always followed by the view actually taking over the screen (and viewDidAppear: being called), because this is a situation in which those things might not happen. (As Apple says in the WWDC 2013 videos, "view will appear" actually means "view might appear".)
There is a secondary set of animations, namely, everything connected with the navigation bar - the change of title (it is supposed to fade into view) and, in this case, the change between not hidden and hidden. The runtime is trying to coordinate the secondary set of animations with the sliding view animation. But you have made that difficult by calling for no animation when the bar is hidden or shown.
Thus, as you've already been told, one solution is to change animated:NO to animated:YES throughout your code. This way, the showing and hiding of the navigation bar is ordered up as part of the animation. Therefore, when the gesture is cancelled and the animation is run backwards to the start, the showing/hiding of the navigation is also run backwards to the start - the two things are now staying coordinated.
But what if you really don't want to make that change? Well, another solution is to change viewWillAppear: to viewDidAppear: throughout. As I've already said, viewWillAppear: is called at the start of the animation, even if the gesture won't be completed, which is causing things to get out of whack. But viewDidAppear: is called only if the gesture is completed (not canceled) and when the animation is already over.
Which of those two solutions do I prefer? Neither of them! They both force you to make changes you don't want to make. The real solution, it seems to me, is to use the transition coordinator.
The Transition Coordinator
The transition coordinator is an object supplied by the system for this very purpose, i.e., to detect that we're involved in an interactive transition and to behave differently depending on whether it is canceled or not.
Concentrate just on the OneViewController implementation of viewWillAppear:. This is where things are getting messed up. When you're in TwoViewController and you start the pan gesture from the left, OneViewController's viewWillAppear: is being called. But then you cancel, letting go of the gesture without completing it. In just that one case, you want not to do what you were doing in OneViewController's viewWillAppear:. And that is exactly what the transition coordinator allows you to do.
Here, then, is a rewrite of OneViewController's viewWillAppear:. This fixes the problem without your having to make any other changes:
-(void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
id<UIViewControllerTransitionCoordinator> tc = self.transitionCoordinator;
if (tc && [tc initiallyInteractive]) {
[tc notifyWhenInteractionEndsUsingBlock:
^(id<UIViewControllerTransitionCoordinatorContext> context) {
if ([context isCancelled]) {
// do nothing!
} else { // not cancelled, do it
[self.navigationController setNavigationBarHidden:YES animated:NO];
}
}];
} else { // not interactive, do it
[self.navigationController setNavigationBarHidden:YES animated:NO];
}
}
The fix is simple , but I don't have any explanation at the moment why this is happening.
One your OneViewController change your viewWillAppear to ,
-(void)viewWillAppear:(BOOL)animated{
// [self.navigationController setNavigationBarHidden:YES animated:NO];
self.navigationController.navigationBar.hidden = YES;
}
and on the second and third view controllers change it to,
-(void)viewWillAppear:(BOOL)animated{
//[self.navigationController setNavigationBarHidden:NO animated:NO];
self.navigationController.navigationBar.hidden = NO;
}
Strange but this will fix the issue when we directly use the hidden property of the UINavigationBar.
I don't know how do you make "FirstViewController has the navigation bar hidden".
I have the same problem, and I fixed it by replacing
self.navigationController.navigationBarHidden = YES / NO;
by
[self.navigationController setNavigationBarHidden:YES / NO animated:animated];
I gave up trying to make this work used my own swipe recognizer that pops the navigation stack:
override func viewDidLoad() {
super.viewDidLoad()
// disable system swipe back gesture and add our own
navigationController?.interactivePopGestureRecognizer?.enabled = false
let swipeBackGestureRecognizer = UISwipeGestureRecognizer(target: self, action: "swipeBackAction:")
swipeBackGestureRecognizer.direction = UISwipeGestureRecognizerDirection.Right
tableView.addGestureRecognizer(swipeBackGestureRecognizer)
}
func swipeBackAction(sender: UISwipeGestureRecognizer) {
navigationController?.popViewControllerAnimated(true)
}
Disable the system interactivePopGestureRecognizer
Create your own UISwipeGestureRecognizer with a Right direction
Pop the navigation stack animated when he swipe is detected
Here's what fixed it for me (Swift)
1st view controller:
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.setNavigationBarHidden(true, animated: animated)
}
2nd and 3rd view controllers:
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.setNavigationBarHidden(false, animated: animated)
}

UINaviagtionBar subview doesn't fade out during UIViewController "Back" transition

I have a UINavigationBar based app. I've created a custom UIView with some titles and added it as a subview to the navigation bar:
[self.navigationController.navigationBar addSubview:_navbarView];
Everything works ok until I hit the back button in the navigation bar and the UIViewController transition occurs.
The problem is that my custom view doesn't fade away like the others elements in the UINavigationBar, it just stays the same and disappears when the transition is complete.
I want it to fade away during the transition like the native elements of the UINavigationBar, is there any way to achieve this?
If you add a subview to the navigation bar, then it will just stay there; the navigation controller doesn't know to do anything special with it. You say your custom view has "some titles" - have you tried doing this instead?
self.navigationItem.titleView = _navbarView;
Then the navigation controller knows that the view should be used in place of your controller's title, and it should animate in and out.
If that doesn't work, you'll need to look at becoming the navigation controller's delegate. Since iOS7, this can get quite complex.
If you need custom navigation bar it could be a good idea to create UINavigationController with custom UINavigationBar
- (instancetype)initWithNavigationBarClass:(Class)navigationBarClass toolbarClass:(Class)toolbarClass
in your navigation bar class you can implement
- (UINavigationItem *)popNavigationItemAnimated:(BOOL)animated
{
if (animated) {
//your_problem_view animation here
}
[super popNavigationItemAnimated];
}

How to handle backBarButtonItem pressed?

I have almost done this in all the application but I have 3 views stacked in navigationController and I need to jump from the third view to the first view.
As I understand I can do this via viewWillDisappear only. But if I try this "jump" I will get the navigationController panel from the second View which with a navigation buttons which cause exceptions/errors.
P.S. Do not advice me to make leftBarButtonitem looking like backBarButtonItem. It is too difficult and I don't know where to find an appropriate image for it.
To my knowledge, you have no choice but to provide your own UIBarButtonItem. You are not permitted from interrupting how UINavigationController works by default. That is, you cannot override the behavior of the back button. You must provide a custom bar button item and set it as the navigation item's left bar button item.
(As a side note, the sort of behavior you're looking for may be an indication of a poor navigation pattern. Back buttons should almost always back out of a navigation hierarchy sequentially.)
Let's say in navigation order your views stacked like top -> 3 -> 2 -> 1 . When you are in this position you can have a flag in your application delegate that shows you will doublePop when backButton pressed as below: ( You are doing this whenever third view appears in the order you mentioned)
MyApplicationDelegate * del = [[UIApplication sharedApplication]delegate];
del.doublePopEnabled = YES;
[del release];
In view 2 :
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
MyApplicationDelegate * del = [[UIApplication sharedApplication]delegate];
if(del.doublePopEnabled){
//Asssuming you have a reference to your navigationController in your view 2
del.doublePopEnabled = NO;
[del.release]
//Use animated as no if you don't want user to see doublePopping.
self.navigationController popViewControllerAnimated:NO];
}
}
Hope it helps.

Resources