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)
}
Related
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.
In my app I am partially swiping my ViewController and swiping back to the same view. While doing this viewDidLoad is not getting called. But if I swipe fully and come back to the view again, viewDidLoad gets called. It is creating some issue.
So even if I swipe partially and stay in the same view viewDidLoad needs to be called (currently only viewWillAppear is getting called). How to do this?
Half swipe of the back button is just like a back to the previous viewcontroller. When you half swipe the viewcontroller and again come to that viewcontroller viewdidload will never be called.
Its better you put the swipegesture disable.
if ([self.navigationController respondsToSelector:#selector(interactivePopGestureRecognizer)]) {
self.navigationController.interactivePopGestureRecognizer.enabled = NO;
}
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;
}
I am trying to create a user interface enabling users to switch between the keyboard and other menus when using a chat application.
On a click of the textField bar I want to raise either the keyboard or a collection view.
The problem occurs when I click the 'menu' button. I want the textField bar to raise revealing my menu view. Then, on a click on the keyboard button, instantly switch to the keyboard, rather than having it slide up from the bottom. This means I need to have the keyboard already loaded and hidden but in the background of the app.
Currently though the earliest I am managing to assign a variable to the keyboard is in the keyboardDidShow function.
-(void) keyboardDidShow: (NSNotification *) notification {
// Get the window the keyboard is a subview of
_window = [UIApplication sharedApplication].windows.lastObject;
_keyboard = _window.subviews[0];
}
This means that after it has been loaded once I can hide and reveal it, but I don't want it visible when it is loading this first time.
To achieve this using alternate means I have tried adding my extra views as subviews of the UIWindow the keyboard is created in:
[_window addSubview:_menuView];
[_window addSubview:_gamesView];
[_window addSubview:_stickerView];
[self hideSpecificView];
Unfortunately I keep coming across the same problem, until I have loaded the keyboard once it needs to fully load before I can get a pointer to it to hide it.
Here is a picture of my toolBar incase I am not being clear:
On clicking the menu icon or the stickers icon I want the bar to raise with a collection view. If I then click the textfield, with these views visible, I want to hide the visible view to immediately show the keyboard behind.
I have also tried experimenting with keyboardWillShow but as the window hasn't been loaded in front our screen I can't get a pointer to the keyboard to hide it before it loads.
An example of what I am after can be found many chat apps (facebook messenger, LINE, Kakao Talk)
Any help would be greatly appreciated
Although the way I came up with isn't perfect it works almost perfectly so hopefully this might help people in the future. If anyone else has solved it differently please post as it would be interesting to know how you did it.
I started by adding a class variable to a UIWindow in my header file and then setting off a timer to ping just after the keyboard will show method finishes. After this method has finished the keyboard has been created, just, and so I allocate it and hide it.
-(void) keyboardWillShow: (NSNotification *) notification {
// More keyboard code
_window = [UIApplication sharedApplication].windows.lastObject;
[NSTimer scheduledTimerWithTimeInterval:0.01
target:self
selector:#selector(allocateKeyboard)
userInfo:nil
repeats:NO];
}
- (void)allocateKeyboard {
if (!_keyboard) {
_keyboard = _window.subviews[0];
}
_keyboard.hidden = YES;
[self setViewForButtonType];
}
I have already previously added my other views, hidden them and constrained them to the bottom of the main view, this means that when the keyboard rises they do too.
- (void)viewDidLoad {
[self.view addSubview:_menuView];
[self.view addSubview:_gamesView];
[self.view addSubview:_stickerView];
}
...
- (void)hideViews {
_keyboard.hidden = YES;
_menuView.hidden = YES;
_gamesView.hidden = YES;
_stickerView.hidden = YES;
}
When buttons get pressed I simple then unhide the view that I want to see and hide the rest of the views.
When I say that this method doesn't work perfectly it is because if you load view main view and then click a button before the keyboard has loaded for the first time then you get a quick glimpse of the keyboard before the view appears over the top. This though only happens the first time and only if they don't click in the text field first.
Anyway, I found this was the best way of making views look like they are in front of the keyboard. Obviously my code was a lot longer and more complex (too long for here) but this is the gist of the method I used to solve it. Comment if you have any queries and I hope this helps.
I have a problem with my table view. When dismissing a modal view controller presented on top of it, it always scrolling to the top . I have tried observing the changes to contentOffset using KVO, but the one that messes my view goes behind it.
From the UITableViewController, when user finishes his task in the modal dialog, self.tableView.contentOffset is , I call:
[self dismissModalViewControllerAnimated:YES]
Subsequently, when the viewWillAppear:(BOOL)animated is called, the self.tableView.contentOffset is already set to 0,0.
Is this supposed to be happening? I am able to work around the issue by remembering the scroll position before presenting the modal view and restore it back in viewWillAppear after dismissing the modal view. But it seems wrong. Am I missing something?
I have found similar problem described in Dismiss modal view changes underlying UIScrollView.
It looks like this is default behavior of UITableViewController. I tested it in very simple app and It worked exactly as you said. If you don't like it, use UIViewController instead.
Here is how I work around this problem, so that the table view maintains the original scroll position. In my subclass of UITableViewController I have added:
#property (assign) CGPoint lastScrollPosition;
Then in the implementation, I have overridden the following:
- (void)viewWillAppear:(BOOL)animated
{
self.tableView.contentOffset = self.lastScrollPosition;
}
- (void)dismissModalViewControllerAnimated:(BOOL)animated
{
self.lastScrollPosition = self.tableView.contentOffset;
[super dismissModalViewControllerAnimated:animated];
}
If you want your table to initially appear scrolled to non-zero position, as I did, don't forget to initialize the lastScrollPosition in your viewDidLoad.