Disable swipe back gesture in Swift - ios

Been looking around on here for a while but can't seem to find a working solution.
I'm trying to disable the swipe to go back to previous view gesture, in Swift.
I've tried a variety of solutions including:
self.navigationController?.interactivePopGestureRecognizer.enabled = false
and
self.navigationController.interactivePopGestureRecognizer.delegate = self
func gestureRecognizerShouldBegin(gestureRecognizer: UIGestureRecognizer!) -> Bool {
return false
}
Is there a new method of doing this or some other method that works?

The following is an easy approach to disabling & re-enabling the swipe back.
Swift 3.x & up
In a viewDidLoad/willAppear/didAppear method add:
navigationController?.interactivePopGestureRecognizer?.isEnabled = false
Just keep in mind that if you do it with viewDidLoad, then the next time you open the view, it may not be set depending upon whether or not it remains in your stack.
Unless you want it to remain off, you will need to turn it back on when the view is closed via either willMove(toParentViewController:) or willDisappear. Your navigationController will be nil at viewDidDisappear, so that is too late.
navigationController?.interactivePopGestureRecognizer?.isEnabled = true
A special note on SplitViewControllers:
As pointed out by CompC in the comments, you will need to call the second navigation controller to apply it to a detail view as such:
navigationController?.navigationController?.interactivePopGe‌​stureRecognizer?.isE‌​nabled = false
Swift 2.2 & Objective-C
Swift versions 2.x & below:
navigationController?.interactivePopGestureRecognizer?.enabled
Objective-C:
self.navigationController.interactivePopGestureRecognizer.enabled

You could disable it but that would not be to recommended as most iOS users go back by swiping and less by pressing the back button.
If you want to disable it it would be more reasonable to use a modal segue instead of a push segue which is not that big of a transfer.
If you really want to get rid of the swipe to go back function I would just disable the back button and have a done button on the top right of the screen.
self.navigationController?.navigationItem.backBarButtonItem?.isEnabled = false;

I was able to do this by returning false in gestureRecognizerShouldBegin
class ViewController2: UIViewController, UIGestureRecognizerDelegate {
...
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.navigationController?.interactivePopGestureRecognizer.delegate = self
}
func gestureRecognizerShouldBegin(gestureRecognizer: UIGestureRecognizer) -> Bool {
return false
}

Add this line before pushing view controller to navigation controller
self.navigationController?.interactivePopGestureRecognizer?.isEnabled = false

Nothing wrong with either answer from Hari or Stefan but this is more succinct. Just put it in viewDidLoad and you're done.
if navigationController!.respondsToSelector(Selector("interactivePopGestureRecognizer")) {
navigationController!.view.removeGestureRecognizer(navigationController!.interactivePopGestureRecognizer)
}
EDIT:
One small caveat is that if the Navigation Controller was opened by another view and the Navigation Controller is closed then you'll get an EXC_BAD_ACCESS error. To fix it you have to save the original UIGestureRecognizer and put it back when you exit the view.
Declare:
private var popGesture: UIGestureRecognizer?
Immediately before removing the gesture:
popGesture = navigationController!.interactivePopGestureRecognizer
Then when closing the view:
If popGesture != nil {
navigationController!.view.addGestureRecognizer(popGesture!)
}

RowanPD's logic for Swift 4
private var popGesture: UIGestureRecognizer?
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if navigationController!.responds(to: #selector(getter: UINavigationController.interactivePopGestureRecognizer)) {
self.popGesture = navigationController!.interactivePopGestureRecognizer
self.navigationController!.view.removeGestureRecognizer(navigationController!.interactivePopGestureRecognizer!)
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if let gesture = self.popGesture {
self.navigationController!.view.addGestureRecognizer(gesture)
}
}

Instead of
self.navigationController.pushViewController(VC, animated: Bool)
call
self.navigationController.setViewContollers([VC], animated: Bool)
setViewControllers replaces the all the VCs on the stack, instead of adding a new controller on top. This means that the new set VC is the root VC, and the user cannot go back.
This is most effective when you only want to disable the swipe on a single VC, and keep the swipe-to-back for the other VC.
If you want users to be able to go back, just not through swiping, do not use this method as it will disable all backs (as there is no VC to go back to).

for objective -c
-(void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:true];
self.navigationController.interactivePopGestureRecognizer.enabled = NO;
}

This is something you missed if it doesn't work after you tried all.
Add navigationController?.interactivePopGestureRecognizer?.isEnabled = false
to your viewWillAppear(animated:) method.
if it doesn't work, remove navigation delegate from the view controller. Check again if your view controller is confirming UINavigationControllerDelegate, UIGestureRecognizerDelegate protocols. if so, just remove it.

I generally make sure that swipe back is enabled in as many places as possible, even adding a custom gesture recognizer to add it to modal screens. However for an authentication and download process in my app I start the process with a modal navigation controller and then push the view for each next step. However, once it's completed I want to prevent them from backing up into the authentication screens.
For this scenario I've been using:
navigationController?.interactivePopGestureRecognizer?.isEnabled = false
navigationItem.hidesBackButton = true
in viewWillAppear() on the final screen. You can undo these in viewWillDisappear() if you're pushing another view and need them there.

Come here a little bit late. In my case self.navigationController?.navigationItem.backBarButtonItem?.isEnabled = false; not working. So I do this: you can present view controller instead of push view controller. This way the swipe back gesture will not apply to the view controller.
navigationController?.present(vc, animated: true)
You could use dismiss for your custom back button
self.dismiss(animated: true)
Note: You could set VC modal presentation style before present it to make sure it's full screen.
vc.modalPresentationStyle = .fullScreen
Hope this help.

If requirement is to show side menu on some of the screens then add AddScreenEdgePanGesture on this specific view instead of navigationController view
replace it
SideMenuManager.default.menuAddScreenEdgePanGesturesToPresent(toView: self.navigationController?.view)
with this
SideMenuManager.default.menuAddScreenEdgePanGesturesToPresent(toView: self.view)

Only complete removal of the gesture recognizer worked for me (from the presenting view controller).
if let navigationController = parent.navigationController,
let interactivePopGestureRecognizer = navigationController.interactivePopGestureRecognizer {
navigationController.view.removeGestureRecognizer(interactivePopGestureRecognizer)
}

Don't Use this if you don't want to come back, or you set the new rootViewController.
self.navigationController.pushViewController(VC, animated: Bool)
Use this
self.navigationController.setViewContollers([VC], animated: Bool)
setViewControllers Remove all the View Controllers on the stack then the user cannot go back. it will disable all backs

this worked for me
gesture(DragGesture(minimumDistance: 10, coordinateSpace: .global))
so minimum distance is the distance to which drag gesture start listening, setting to 0 removes any listening, but it will remove all interactions be aware, i have changed 0 to 10 to listen to tap gestures, but in your screen if you have any other interaction it will not work after adding this,

If you don't care about system back button appearance (for example, if you're using custom back button or navigation bar is hidden at all), it might help you:
navigationItem.hidesBackButton = true
It hides back button and disables swipe back gesture.

Related

Convert modal view to fullscreen view

I have a modal view controller A presented at the top of the view controllers hierarchy.
Is it possible to make it fullscreen temporarily when a user presses a button? And then go back to standard modal look when used presses another button?
To clarify, I'm not talking about modalPresentationStyle = .fullScreen BEFORE A is presented, I need to present A in a standard way and then stretch it to be fullscreen when needed.
Although if I write pseudo code it would be something like this:
class A: UIViewController {
#objc func goFullScreen(sender: UIButton) {
// pseudo code:
// modalPresentationStyle = .fullScreen
// go fullscreen, block `drag-to-dismiss` gesture, remove `cornerRadius`, set `self.view.frame = UIScreen.main.bounds`
}
#objc func cancelFullScreen(sender: UIButton) {
// pseudo code:
// modalPresentationStyle = .pageSheet
// return to the standard look of a modal view controller
}
}
I found UIAdaptivePresentationControllerDelegate.presentationControllerShouldDismiss method that intercepts drag to close gesture, but I don't see any way to go fullscreen.
Picture of what I am talking about:
ps. I know I can make a custom container view controller with a custom transition delegate but I'd like to know if it's possible to implement such behaviour with system classes because I basically need just to flip modalPresentationStyle property with some interpolation animation...
Also on the right I hide/show some UI elements, but still it's the same view controller

UIPanGestureRecognizer to pop UIViewController

I'm wondering if it is actually possible to use a UIPanGestureRecognizer on a pushed UIViewController to achieve a similar behaviour like in the Telegram messenger chat view (and a lot of other popular Apps), where you can simply swipe to the right from anywhere on the screen to get back to the menu (or any other View Controller that initially pushed the one we are looking at).
I tried this code:
#objc func swipeLeft(_ sender: UIPanGestureRecognizer) {
let point = sender.translation(in: view)
containerView.center = CGPoint(x: point.x > 0 ? view.center.x + point.x : view.center.x, y: view.center.y)
if sender.state != .ended { return }
if containerView.center.x < view.frame.width / 2 {
dismissSelf()
}
else {
UIView.animate(withDuration: 0.2) {
self.containerView.center = self.view.center
}
}
}
and a UIPanGestureRecognizer, which does work nicely if you presented your ViewController but not when it is pushed. At least not how it is right now.
Right now, you see a black view and that's also what you see in the "Debug View Hirachy" at the bottom of a pushed UIViewController.
Any help is appreciated!
I think what you are looking for is already built-in with the interactivePopGestureRecognizer
self.navigationController?.interactivePopGestureRecognizer?.isEnabled = true
if you want to make some custom or different animation then I think you need to check transitions. Here's a good article to make custom transitions:
https://medium.com/swift2go/simple-custom-uinavigationcontroller-transitions-fdb56a217dd8
No need of handling pan gesture.
you could just embed your view in a navigation controller, and it will provide such behaviour (swipe to go back).
Then you could also hide the navigation bar if you dont want to see it.
The user can also remove the topmost view controller using the back
button in the navigation bar or using a left-edge swipe gesture.
https://developer.apple.com/documentation/uikit/uinavigationcontroller
// Hide the Navigation Bar
self.navigationController?.setNavigationBarHidden(true, animated: animated)
// Show the Navigation Bar
self.navigationController?.setNavigationBarHidden(false, animated: animated)
I've just created a Pod to have this Telegram/Instagram-like Pan-to-pop behavior on the navigation controller.
You can see it here
It allows the user to:
Pan-to-pop normally from the left edge (like every normal UINavigationController)
Pan-to-pop from the center where there is no scrollView or other panGesture that interferes
Pan-top-pop on top of any scrollView if they are at offset.x = 0 (so it behaves like Instagram)
All of this while keeping all the default functionality of the navigation controller.
To install it with CocoaPods just include the pod in the Podfile:
pod 'EZCustomNavigation', '1.0.0'
And to use it just use EZNavigationController instead of the default UINavigationController and it should just work.

Navigation bar buttons set during viewDidLoad don't appear until after view has appeared in iOS 11

In iOS 10, I could determine the list of navigation bar buttons I want to appear in viewDidLoad, and they would appear in the navigation bar as the view controller transitioned in.
In iOS 11, I can no longer do this. Whatever navigation bar buttons were set in interface builder are displayed as the view controller transitions in, and my custom list of buttons are not displayed until after the view finishes sliding in. Updating the buttons in viewWillAppear does not help.
Is this simply a bug in iOS 11, or is there something else I should be doing? Is there a workaround so I can continue to display buttons while the screen loads?
In the following example, I have set a button "Default Button" in the storyboard, and override it with an "Edit" button in viewDidLoad. The project is available on Github.
iOS 10
iOS 11
It looks like the issue is that navigation bar icons displayed during the transition appear to be fixed when the view controller is passed off to the navigation controller. By the time that viewDidLoad is called, the icons have already been fixed.
In order to fix this, we need to update the navigation bar icons on the view controller's navigationItem before the view controller is pushed onto the navigation controller stack.
One way to do this would be to setup the navigation bar icons in awakeFromNib. This is what #Joe's answer was effectively doing, because apparently viewDidLoad is called from awakeFromNib when isViewLoaded is true during awakeFromNib.
However, doing this in awakeFromNib prevents you from taking into account any properties set on the view controller in prepareForSegue. So another option (and the one that I am using) is to force the entire view to load in prepareForSegue by adding the line _ = controller.view after setting any desired properties.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "showDetail" {
if let indexPath = tableView.indexPathForSelectedRow {
let controller = (segue.destination as! UINavigationController).topViewController as! DetailViewController
...
_ = controller.view
}
}
}
Move your Edit barButton code from viewDidLoad to isViewLoaded method as below.
override var isViewLoaded: Bool {
self.navigationItem.rightBarButtonItem = self.editButtonItem
return true
}
Output:
Note: Above code will fix the transition delay issue. Really, don't have much to explain why this happening. I experienced similar issue in iOS 10 as well NavigationBar delay updating barTintColor iOS10. It could be another bug in iOS11.
PS:
After reading Apple Doc about isViewLoaded and comparing other view loading methods. Its all about loading memory upon view loads.
You don't really need to move you barButton code at all.Just implement the isViewLoaded method and return to true as below:
override var isViewLoaded: Bool { return true}

swift hiding navbar in mmdrawercontroller

I have created a slidout menu for kmy app with mmdrawercontroller from github.
It all works perfectly but i can't get rid of the nav bar at the top. it is overlapping part of my many but also shows up on all other pages. I need it gone. i have found the following code
navigationController?.setNavigationBarHidden(navigationController?.navigationBarHidden == false, animated: true)
But i have no idea where to place it. I tried it in appdelegate, in the leftsliderviewcontroller, in the button who invokes the mmdrawercontroller. But nothing seems to work.
Also in code that comes with the mmdrawercontroller i can't find anywhere a bool for this setting.
Can any one show me how to hide this navigationbar.
thanks.
If you want to hide your UINavigationBar in some UIViewControllers the you need to call self.navigationController?.navigationBarHidden = true in your viewWillAppear like in the following way:
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.navigationBarHidden = true
}
And the above code hide your UINavigationBar. I hope this help you.

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

Resources