Issue with Modal present and dismiss - ios

I have a ViewController that has 20 points padding around the edge of the main content. This is clear colour to be transparent.
I then use the following to open it as a Modal.
noteDetailController.modalPresentationStyle = .overCurrentContext
present(noteDetailController, animated: false, completion: nil)
My idea was that I would set a UITapGestureRecognizer to allow the user to tap anywhere in the padding area to dismiss the Modal.
The following is hit by this UITapGestureRecognizer, inside the Modal ViewController
self.parentController?.dismiss(animated: false, completion: nil)
I'm having issues where the ViewController underneath the Modal seems to be responding the pressing the screen and there's a major lag in loading and dismissing Modals.
Does this logic seem correct, am I doing things in a way that you would expect to work? It almost seems like the present and dismiss start happening out of sync and several Modals are loaded.
Thanks for your time.

Your view controller underneath should not be getting touch events while another view controller is placed above it.
Make a segue to the modal controller as Kind: Present Modally, Presentation: Over Current Context, and call the segue as performSegue(withIdentifier: "showModal", sender: nil)
Make sure the view of the modal view controller has checked "User Interaction Enabled" and has a clear color background.
Connect the container view of the modal to the modal view controller with an IBOutlet, add a UITapGestureRecognizer to the modal's view (padding) and implement a UIGestureRecognizerDelegate method to detect that the tap was indeed only in padding and not in the container view itself:
#IBOutlet weak var containerView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
let tap = UITapGestureRecognizer(target: self, action: #selector(hide(gr:)))
tap.delegate = self
view.addGestureRecognizer(tap)
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
if let v = touch.view, v.isDescendant(of: containerView) {
return false
}
return true
}
#objc func hide(gr: UITapGestureRecognizer) {
dismiss(animated: true)
}

Related

Disabling interactive dismissal of presented view controller on iOS 13 when dragging from the main view [duplicate]

In iOS 13 modal presentations using the form and page sheet style can be dismissed with a pan down gesture. This is problematic in one of my form sheets because the user draws into this box which interferes with the gesture. It pulls the screen down instead of drawing a vertical line.
How can you disable the vertical swipe to dismiss gesture in a modal view controller presented as a sheet?
Setting isModalInPresentation = true still allows the sheet to be pulled down, it just won't dismiss.
In general, you shouldn't try to disable the swipe to dismiss functionality, as users expect all form/page sheets to behave the same across all apps. Instead, you may want to consider using a full-screen presentation style. If you do want to use a sheet that can't be dismissed via swipe, set isModalInPresentation = true, but note this still allows the sheet to be pulled down vertically and it'll bounce back up upon releasing the touch. Check out the UIAdaptivePresentationControllerDelegate documentation to react when the user tries to dismiss it via swipe, among other actions.
If you have a scenario where your app's gesture or touch handling is impacted by the swipe to dismiss feature, I did receive some advice from an Apple engineer on how to fix that.
If you can prevent the system's pan gesture recognizer from beginning, this will prevent the gestural dismissal. A few ways to do this:
If your canvas drawing is done with a gesture recognizer, such as your own UIGestureRecognizer subclass, enter the began phase before the sheet’s dismiss gesture does. If you recognize as quickly as UIPanGestureRecognizer, you will win, and the sheet’s dismiss gesture will be subverted.
If your canvas drawing is done with a gesture recognizer, setup a dynamic failure requirement with -shouldBeRequiredToFailByGestureRecognizer: (or the related delegate method), where you return NO if the passed in gesture recognizer is a UIPanGestureRecognizer.
If your canvas drawing is done with manual touch handling (e.g. touchesBegan:), override -gestureRecognizerShouldBegin on your touch handling view, and return NO if the passed in gesture recognizer is a UIPanGestureRecognizer.
With my setup #3 proved to work very well. This allows the user to swipe down anywhere outside of the drawing canvas to dismiss (like the nav bar), while allowing the user to draw without moving the sheet, just as one would expect.
I cannot recommend trying to find the gesture to disable it, as it seems to be rather dynamic and can reenable itself when switching between different size classes for example, and this could change in future releases.
This gesture can be found in the modal view controller's presentedView property. As I debugged, the gestureRecognizers array of this property has only one item and printing it resulted in something like this:
UIPanGestureRecognizer: 0x7fd3b8401aa0
(_UISheetInteractionBackgroundDismissRecognizer);
So to disable this gesture you can do like below:
let vc = UIViewController()
self.present(vc, animated: true, completion: {
vc.presentationController?.presentedView?.gestureRecognizers?[0].isEnabled = false
})
To re-enable it simply set isEnabled back to true:
vc.presentationController?.presentedView?.gestureRecognizers?[0].isEnabled = true
Note that iOS 13 is still in beta so a simpler approach might be added in an upcoming release.
Although this solution seems to work at the moment, I would not recommend it as it might not work in some situations or might be changed in future iOS releases and possibly affect your app.
Use this in the presented ViewController viewDidLoad:
if #available(iOS 13.0, *) {
self.isModalInPresentation = true
}
In my case, I have a modal screen with a view that receives touches to capture customer signatures.
Disabling the gesture recognizer in the navigation controller solved the problem, preventing the modal interactive dismissal from being triggered at all.
The following methods are implemented in our modal view controller, and are called via delegate from our custom signature view.
Called from touchesBegan:
private func disableDismissalRecognizers() {
navigationController?.presentationController?.presentedView?.gestureRecognizers?.forEach {
$0.isEnabled = false
}
}
Called from touchesEnded:
private func enableDismissalRecognizers() {
navigationController?.presentationController?.presentedView?.gestureRecognizers?.forEach {
$0.isEnabled = true
}
}
Here is a GIF showing the behavior:
This question, flagged as duplicate, describes better the issue I had: Disabling interactive dismissal of presented view controller on iOS 13 when dragging from the main view
you can change the presentation style, if its in full screen the pull down to dismiss would be disabled
navigationCont.modalPresentationStyle = .fullScreen
No need to reinvent the wheel. It is as simple as adopting the UIAdaptivePresentationControllerDelegate protocol on your destinationViewController and then implement the relevant method:
func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
return false
}
For example, let's suppose that your destinationViewController is prepared for segue like below:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "yourIdentifier",
let destinationVC = segue.destination as? DetailViewController
{
//do other stuff
destinationVC.presentationController?.delegate = destinationVC
}
}
Then on the destinationVC (that should adopt the protocol described above), you can implement the described method func presentationControllerShouldDismiss(_ presentationController:) -> Bool or any of the other ones, in order to handle correctly your custom behaviour.
You can use the UIAdaptivePresentationControllerDelegate method presentationControllerDidAttemptToDismiss and disable the gestureRecognizer on the presentedView.
Something like this:
func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {
presentationController.presentedView?.gestureRecognizers?.first?.isEnabled = false
}
For every body having problems with Jordans solution #3 running.
You have to look for the ROOT viewcontroller which is beeing presented, depending on your viewstack, this is maybe not you current view.
I had to look for my navigation controllers PresentationViewController.
BTW #Jordam: Thanks!
UIGestureRecognizer *gesture = [[self.navigationController.presentationController.presentedView gestureRecognizers] firstObject];
if ([gesture isKindOfClass:[UIPanGestureRecognizer class]]) {
UIPanGestureRecognizer * pan = (UIPanGestureRecognizer *)gesture;
pan.delegate = self;
}
You may first get a reference to the UIPanGestureRecognizer handling the page sheet dismissal in viewDidAppear() method. Notice that this reference is nil in viewWillAppear() or viewDidLoad(). Then you simply disable it.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
presentationController?.presentedView?.gestureRecognizers?.first.isEnabled = false
}
If you want more customization rather than disabling it completely, for example, when using a navBar within the page sheet, set the delegate of that UIPanGestureRecognizer to your own view controller. That way, you can disable the gesture recognizer exclusively in your contentView while keeping it active in your navBar region by implementing
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {}
in IOS 13
if #available(iOS 13.0, *) {
obj.isModalInPresentation = true
} else {
// Fallback on earlier versions
}
Me, I use this :
-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
for(UIGestureRecognizer *gr in self.presentationController.presentedView.gestureRecognizers) {
if (#available(iOS 11.0, *)) {
if([gr.name isEqualToString:#"_UISheetInteractionBackgroundDismissRecognizer"]) {
gr.enabled = false;
}
}
}
Will try to describe method 2 already suggested by #Jordan H in more details:
1) To be able to catch and make decisions about the modal sheet's pan gesture add this into view controller's viewDidLoad:
navigationController?.presentationController?.presentedView?.gestureRecognizers?.forEach {
$0.delegate = self
}
2) Enable the ability to catch the pan gesture together with your own gestures using gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:)
3) The actual decision can go in gestureRecognizer(_:shouldBeRequiredToFailBy:)
Example code, which makes the swipe gesture to be preferred over sheet's pan gesture, if both present. It doesn't affect original pan gesture in areas where there is no swipe gesture recognizer and therefore the original "swipe to dismiss" can still work as designed.
extension PeopleViewController: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer === UIPanGestureRecognizer.self && otherGestureRecognizer === UISwipeGestureRecognizer.self {
return true
}
return false
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
}
In my case I have only a few swipe gesture recognizers, so comparing types is enough for me, but if there more of them it might make sense to compare the gestureRecognizers themselves (either programmatically added ones or as outlets from interface builder) as described in this doc: https://developer.apple.com/documentation/uikit/touches_presses_and_gestures/coordinating_multiple_gesture_recognizers/preferring_one_gesture_over_another
Here's how the code works in my case. Without it the swipe gesture was mostly ignored and worked only occasionally.
In the case when a UITableView or UICollectionView initiates the page sheet dismiss gesture when the user attempts to scroll past the top end of the scrolling view, this gesture can be disabled by adding an invisible UIRefreshControl that calls endRefreshing immediately.
See also https://stackoverflow.com/a/58676756/2419404
SwiftUI since iOS 15
.interactiveDismissDisabled()
For Example:
.sheet(isPresented: $add) {
AddView()
.interactiveDismissDisabled()
}
For navigation Controller, to avoid swipe interaction for presented view we can use:
if #available(iOS 13.0, *) {navController.isModalInPresentation = true}
In prepare(for:sender:) :
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == viewControllerSegueID {
let controller = segue.destination as! YourViewController
controller.modalPresentationStyle = .fullScreen
}
}
or, after you initialize your controller:
let controller = YourViewController()
controller.modalPresentationStyle = .fullScreen

Disable gesture to pull down form/page sheet modal presentation

In iOS 13 modal presentations using the form and page sheet style can be dismissed with a pan down gesture. This is problematic in one of my form sheets because the user draws into this box which interferes with the gesture. It pulls the screen down instead of drawing a vertical line.
How can you disable the vertical swipe to dismiss gesture in a modal view controller presented as a sheet?
Setting isModalInPresentation = true still allows the sheet to be pulled down, it just won't dismiss.
In general, you shouldn't try to disable the swipe to dismiss functionality, as users expect all form/page sheets to behave the same across all apps. Instead, you may want to consider using a full-screen presentation style. If you do want to use a sheet that can't be dismissed via swipe, set isModalInPresentation = true, but note this still allows the sheet to be pulled down vertically and it'll bounce back up upon releasing the touch. Check out the UIAdaptivePresentationControllerDelegate documentation to react when the user tries to dismiss it via swipe, among other actions.
If you have a scenario where your app's gesture or touch handling is impacted by the swipe to dismiss feature, I did receive some advice from an Apple engineer on how to fix that.
If you can prevent the system's pan gesture recognizer from beginning, this will prevent the gestural dismissal. A few ways to do this:
If your canvas drawing is done with a gesture recognizer, such as your own UIGestureRecognizer subclass, enter the began phase before the sheet’s dismiss gesture does. If you recognize as quickly as UIPanGestureRecognizer, you will win, and the sheet’s dismiss gesture will be subverted.
If your canvas drawing is done with a gesture recognizer, setup a dynamic failure requirement with -shouldBeRequiredToFailByGestureRecognizer: (or the related delegate method), where you return NO if the passed in gesture recognizer is a UIPanGestureRecognizer.
If your canvas drawing is done with manual touch handling (e.g. touchesBegan:), override -gestureRecognizerShouldBegin on your touch handling view, and return NO if the passed in gesture recognizer is a UIPanGestureRecognizer.
With my setup #3 proved to work very well. This allows the user to swipe down anywhere outside of the drawing canvas to dismiss (like the nav bar), while allowing the user to draw without moving the sheet, just as one would expect.
I cannot recommend trying to find the gesture to disable it, as it seems to be rather dynamic and can reenable itself when switching between different size classes for example, and this could change in future releases.
This gesture can be found in the modal view controller's presentedView property. As I debugged, the gestureRecognizers array of this property has only one item and printing it resulted in something like this:
UIPanGestureRecognizer: 0x7fd3b8401aa0
(_UISheetInteractionBackgroundDismissRecognizer);
So to disable this gesture you can do like below:
let vc = UIViewController()
self.present(vc, animated: true, completion: {
vc.presentationController?.presentedView?.gestureRecognizers?[0].isEnabled = false
})
To re-enable it simply set isEnabled back to true:
vc.presentationController?.presentedView?.gestureRecognizers?[0].isEnabled = true
Note that iOS 13 is still in beta so a simpler approach might be added in an upcoming release.
Although this solution seems to work at the moment, I would not recommend it as it might not work in some situations or might be changed in future iOS releases and possibly affect your app.
Use this in the presented ViewController viewDidLoad:
if #available(iOS 13.0, *) {
self.isModalInPresentation = true
}
In my case, I have a modal screen with a view that receives touches to capture customer signatures.
Disabling the gesture recognizer in the navigation controller solved the problem, preventing the modal interactive dismissal from being triggered at all.
The following methods are implemented in our modal view controller, and are called via delegate from our custom signature view.
Called from touchesBegan:
private func disableDismissalRecognizers() {
navigationController?.presentationController?.presentedView?.gestureRecognizers?.forEach {
$0.isEnabled = false
}
}
Called from touchesEnded:
private func enableDismissalRecognizers() {
navigationController?.presentationController?.presentedView?.gestureRecognizers?.forEach {
$0.isEnabled = true
}
}
Here is a GIF showing the behavior:
This question, flagged as duplicate, describes better the issue I had: Disabling interactive dismissal of presented view controller on iOS 13 when dragging from the main view
you can change the presentation style, if its in full screen the pull down to dismiss would be disabled
navigationCont.modalPresentationStyle = .fullScreen
No need to reinvent the wheel. It is as simple as adopting the UIAdaptivePresentationControllerDelegate protocol on your destinationViewController and then implement the relevant method:
func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
return false
}
For example, let's suppose that your destinationViewController is prepared for segue like below:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "yourIdentifier",
let destinationVC = segue.destination as? DetailViewController
{
//do other stuff
destinationVC.presentationController?.delegate = destinationVC
}
}
Then on the destinationVC (that should adopt the protocol described above), you can implement the described method func presentationControllerShouldDismiss(_ presentationController:) -> Bool or any of the other ones, in order to handle correctly your custom behaviour.
You can use the UIAdaptivePresentationControllerDelegate method presentationControllerDidAttemptToDismiss and disable the gestureRecognizer on the presentedView.
Something like this:
func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {
presentationController.presentedView?.gestureRecognizers?.first?.isEnabled = false
}
For every body having problems with Jordans solution #3 running.
You have to look for the ROOT viewcontroller which is beeing presented, depending on your viewstack, this is maybe not you current view.
I had to look for my navigation controllers PresentationViewController.
BTW #Jordam: Thanks!
UIGestureRecognizer *gesture = [[self.navigationController.presentationController.presentedView gestureRecognizers] firstObject];
if ([gesture isKindOfClass:[UIPanGestureRecognizer class]]) {
UIPanGestureRecognizer * pan = (UIPanGestureRecognizer *)gesture;
pan.delegate = self;
}
You may first get a reference to the UIPanGestureRecognizer handling the page sheet dismissal in viewDidAppear() method. Notice that this reference is nil in viewWillAppear() or viewDidLoad(). Then you simply disable it.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
presentationController?.presentedView?.gestureRecognizers?.first.isEnabled = false
}
If you want more customization rather than disabling it completely, for example, when using a navBar within the page sheet, set the delegate of that UIPanGestureRecognizer to your own view controller. That way, you can disable the gesture recognizer exclusively in your contentView while keeping it active in your navBar region by implementing
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {}
in IOS 13
if #available(iOS 13.0, *) {
obj.isModalInPresentation = true
} else {
// Fallback on earlier versions
}
Me, I use this :
-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
for(UIGestureRecognizer *gr in self.presentationController.presentedView.gestureRecognizers) {
if (#available(iOS 11.0, *)) {
if([gr.name isEqualToString:#"_UISheetInteractionBackgroundDismissRecognizer"]) {
gr.enabled = false;
}
}
}
Will try to describe method 2 already suggested by #Jordan H in more details:
1) To be able to catch and make decisions about the modal sheet's pan gesture add this into view controller's viewDidLoad:
navigationController?.presentationController?.presentedView?.gestureRecognizers?.forEach {
$0.delegate = self
}
2) Enable the ability to catch the pan gesture together with your own gestures using gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:)
3) The actual decision can go in gestureRecognizer(_:shouldBeRequiredToFailBy:)
Example code, which makes the swipe gesture to be preferred over sheet's pan gesture, if both present. It doesn't affect original pan gesture in areas where there is no swipe gesture recognizer and therefore the original "swipe to dismiss" can still work as designed.
extension PeopleViewController: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer === UIPanGestureRecognizer.self && otherGestureRecognizer === UISwipeGestureRecognizer.self {
return true
}
return false
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
}
In my case I have only a few swipe gesture recognizers, so comparing types is enough for me, but if there more of them it might make sense to compare the gestureRecognizers themselves (either programmatically added ones or as outlets from interface builder) as described in this doc: https://developer.apple.com/documentation/uikit/touches_presses_and_gestures/coordinating_multiple_gesture_recognizers/preferring_one_gesture_over_another
Here's how the code works in my case. Without it the swipe gesture was mostly ignored and worked only occasionally.
In the case when a UITableView or UICollectionView initiates the page sheet dismiss gesture when the user attempts to scroll past the top end of the scrolling view, this gesture can be disabled by adding an invisible UIRefreshControl that calls endRefreshing immediately.
See also https://stackoverflow.com/a/58676756/2419404
SwiftUI since iOS 15
.interactiveDismissDisabled()
For Example:
.sheet(isPresented: $add) {
AddView()
.interactiveDismissDisabled()
}
For navigation Controller, to avoid swipe interaction for presented view we can use:
if #available(iOS 13.0, *) {navController.isModalInPresentation = true}
In prepare(for:sender:) :
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == viewControllerSegueID {
let controller = segue.destination as! YourViewController
controller.modalPresentationStyle = .fullScreen
}
}
or, after you initialize your controller:
let controller = YourViewController()
controller.modalPresentationStyle = .fullScreen

Navigation Bar's Default behaviour to go back is not working

I have a view controller where I need to add a custom back button. So I have added a custom backButtonItem. But After adding custom back button the default behavior of my view controller to go back by swipe stops working as expected.
If I remove the custom back button from the view controller, the behavior of view controller is as expected but as soon as I add custom back button the default behavior stops.
I have added the custom back button like this
self.navigationItem.leftBarButtonItem = getCustomBackBarButtonItem(viewController: self)
I have tried to use backBarButtonItem instead of leftBarButtonItem, but by doing that the custom back button doesn't appear and the view controller's behavior is as expected.
If I remove the above code the behavior of the view controller is as expected and it smoothly goes back by swipe.
Be sure to build the UIBarButtonItem doing something like this:
let customBack = UIBarButtonItem(title: "Back", style:.done, target:self, action: #selector(self.letsGoBack))
and then implement the pop back function with:
#objc func letsGoBack() {
self.navigationController?.popViewController(animated: true)
}
so at the end it's just:
self.navigationItem.leftBarButtonItem = customBack
if you want to keep the swipe back gesture, you might subclass your navigation controller doing so:
class YourNavigationController: UINavigationController, UIGestureRecognizerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
self.interactivePopGestureRecognizer?.delegate = self
}
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return self.viewControllers.count > 1
}
}
Instead of setting delegate to self you can simply add this line:
self.navigationController?.interactivePopGestureRecognizer?.delegate = nil
So it won't go to any of interactive PopGestureRecognizer's delegate method and the navigation Controller behavior will be as per your expectation. This is the small workaround to achieve the expected behavior.
To navigate back to previous viewController
Initialize UIBarButton
var backBtn:UIBarButtonItem!
add UIBarButton to NavigationBar
backBtn = UIBarButtonItem(title: "Go-Back" , style: .plain, target: self, action: #selector(self.backBtnClicked(_:)))
self.navigationItem.leftBarButtonItem = backBtn
add this function into your class
func backBtnClicked(_ sender:UIBarButtonItem) {
if let redirect = self.navigationController?.popViewController(animated: true) {
// If you open this viewController by using pushViewController this will called
} else {
self.dismiss(animated: true, completion: nil)
// If you open this viewController by using present this will called
}
}
Hope this will help you

Messenger style UITextView input

I'm working on a simple chat as a part of my App.
I have a list of Chats (i.e. friends) in a Table View Controller and Chat View Controller displaying messages for each chat. Both of them are embedded in a NavigationController, which is embedded in a TabBarController.
I want to place UITextView message text input field in a ChatViewController below my UITableVeiw that shows messages of that chat. I would also like the UITextView to look like it's embedded in a tab bar.
I've run through dozens of manuals tutorials and guides and that's where I got so far.
App simulator screenshots here: nice at the top, buggy at the bottom
Main.storyboard screenshot here
I use UITextView instead of UITextField because I want it to be able to change its size depending on content size
I use UIViewController instead of UITableViewController to be able to add another UIView along with UITableView
I don't use actual UITabBar to embed my UITextView because it behaves really weird when UITextView needs to change its height. And as far as I understood Apple doesn't support embedding UI element to UITabBar
What I do is I hide TabBar in ChatViewController viewWillAppear:
override func viewWillAppear(_ animated: Bool) {
tabBarController?.tabBar.isHidden = true
}
And I show it again in parent ChatsTableViewController:
override func viewWillAppear(_ animated: Bool) {
tabBarController?.tabBar.isHidden = false
}
Once TabBar is hidden, my custom stackView embedding textView and sendButton takes its place and operates quite normally as I want it to.
To handle keyboard behaviour properly I have an outlet for my inputStackView bottom constraint (selected on screenshot), which I update on keyboardWillShow/Hide Notifications. Code for that looks like this:
#IBOutlet weak var inputBottomConstraint: NSLayoutConstraint!
var inputBottomConstraintInitialValue: CGFloat!
//MARK: - Keyboard handling
private func enableKeyboardHideOnTap(){
NotificationCenter.default.addObserver(self, selector: #selector(ChatTableViewController.keyboardWillShow(notification:)), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(ChatTableViewController.keyboardWillHide(notification:)), name: NSNotification.Name.UIKeyboardWillHide, object: nil)
let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(ChatTableViewController.hideKeyboard))
self.view.addGestureRecognizer(tap)
}
#objc func hideKeyboard() {
textView.resignFirstResponder()
}
#objc func keyboardWillShow(notification: NSNotification) {
let info = notification.userInfo!
let keyboardFrame: CGRect = (info[UIKeyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
let duration = info[UIKeyboardAnimationDurationUserInfoKey] as! Double
UIView.animate(withDuration: duration) { [weak self] () -> Void in
self?.inputBottomConstraint.constant = keyboardFrame.size.height + 8
self?.view.layoutIfNeeded()
}
}
#objc func keyboardWillHide(notification: NSNotification) {
let duration = notification.userInfo![UIKeyboardAnimationDurationUserInfoKey] as! Double
UIView.animate(withDuration: duration) { [weak self] () -> Void in
guard let `self` = self else { return }
self.inputBottomConstraint.constant = self.inputBottomConstraintInitialValue
self.view.layoutIfNeeded()
}
}
Everything seems to work fine at this point:
TabBar is hidden in a ChatViewController and is shown in ChatsTableViewController
textView and sendButton embedded in a stackView slide along will keyboard up and down, as shown in simulator screenshots above.
The BUG appears when I swipe between ChatViewController and ChatsTableViewController instead of using NavigationControllers
When I start swiping back to ChatsTableViewController, it perform its viewWillAppear method, so tabBarController's tabBar become visible. And if I don't finish that swipe to ChatsTableViewController and go back ChatViewController, ChatViewController's viewWillAppear sets it back to invisible, but the input stackView doesn't get back to it's initial position. It just hangs there above the invisible tabBar.
I've tried moving
tabBarController?.tabBar.isHidden = false
from viewWillAppear to viewDidAppear in ChatsTableViewController.
It fixes the "floating input" issue but it adds a lag of about a second, when ChatsTableViewController is showing without tabBar. Looks not too good either.
Any ideas how that could be fixed properly?
I've been struggling with that issue for about a week))
Because your ChatsTableViewController is embedded in in UINavigationController and your ChatViewController is pushed you can override this hidesBottomBarWhenPushed in ChatViewController which will determine to show your view controller or not:
public override var hidesBottomBarWhenPushed: Bool {
get { return true }
set { super.hidesBottomBarWhenPushed = newValue }
}
This is a better way to hide the tab bar to my mind, because the frame of the view controllers will be adjusted.
Why do you change constraint on keyboard show/Hide. Try to Google inputAccesoryView, many chats use it. It’s natural behaviour is to go with the keyboard. Try FaceBook Messenger for example and see there how swiping hides the keyboard and Textfield together, you can’t accomplish this kind od behaviour with regular view.

iOS UINavigationController barHideOnTapGestureRecognizer and UIButton interference

I have developed an app that makes use of the iOS8 feature to show or hide the navigation bar on a tap of the view.
However, the main view contains a UIButton which also act upon taps. The problem is that both 'objects' are receiving the tap and if I tap the button, the navigation bar toggles its visibility.
I can get to the barHideOnTapGestureRecognizer via the navigation controller but not really sure what can be done with it to stop it responding if a button is tapped.
Is there a way (apart from switching off or changing to 'Swipe to Hide') to subdue the navigation bar's appearance/disappearance when a button is pressed?
Don't use the standard barHideOnTapGestureRecognizer. Fortunately, it's not hard to roll your own:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let gestureRecognizer = UITapGestureRecognizer(target: self, action: "toggleBarsOnTap:")
self.view.addGestureRecognizer(gestureRecognizer)
}
func toggleBarsOnTap(sender: AnyObject?) {
let hidden = !self.navigationBarHidden
self.setNavigationBarHidden(hidden, animated: true)
self.setToolbarHidden(hidden, animated: true)
}
Taps on the view will show/hide the bars, and taps on controls (subviews of the view) will not.
[self.navigationController setNavigationBarHidden:YES];

Resources