iOS 11.2, Xcode 9.2
I've tried all the ways to dismiss keyboard on viewDidDisappear of the old UIViewController AFTER a new UIViewController is pushed into UINavigationController stack. But with no luck.
If I dismiss it on viewWillDisappear - it will be dismissed but with animation DURING push animation. It is not the desired behaviour. I want the old UIViewController's keyboard to be dismissed only when the controller is no longer visible.
The behavior should be like in Telegram app:
In any dialog with visible keyboard press on opponents avatar and you'll be pushed to opponents account information. Then, if you press back button, you'll be redirected back to a dialog. But the keyboard will be already dismissed.
Any help is appreciated!
P.S. The question might look like a duplicate, but I have failed to make it work with the solutions I found.
Edit 1.
I have created a small TEST PROJECT which represents a failure to achieve the desired behavior.
In order to reproduce the undesired behavior:
Launch the app.
Tap on UITextField or UITextView and wait for the keyboard to appear.
Tap on "Next" button and wait for a new controller to be pushed.
Tap on "Back" button and wait for a new controller to be popped.
As a result - the initial view controller will have the active keyboard after the push/pop actions. I need the keyboard to be hidden after the push/pop actions. Also, the keyboard should not be dismissed before the initial view controller becomes invisible, it should be dismissed after viewDidDisappear action.
There are cases where no text field is the first responder but the keyboard is on screen. In these cases, the above methods fail to dismiss the keyboard.
Use the property: isEditable of your textView. Here is a tested code:
override func viewWillAppear(_ animated: Bool) {
self.viewTextView.isEditable = false
super.viewWillAppear(animated)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.viewTextView.isEditable = true
}
Results:
Comments from #iWheelBuy:
Sometimes, text views will have inputAccessoryView. The way you do it
will make the keyboard disappear, but the inputAccessoryView will
remain... That is why you should also make inputAccessoryView = nil
or inputAccessoryView = UIView() when setting isEditable = false
The problem happens because responders are managed (restored, saved) by UIKit between viewWillAppear and viewDidAppear, just before view has appeared, and between viewWillDisapear: and viewDidDisapear:, just before view has disappeared. That is why any change made to responders is visible during animation.
Instead of removing responders, to get the effect you want, you can prevent views from becoming responders before view appears again.
The simplest way to do this for UITextField and UITextView is to temporary disable interaction just before view will appear, and then restore it after the view did reappeared.
override func viewWillAppear(_ animated: Bool) {
self.viewTextField.isUserInteractionEnabled = false
self.viewTextView.isUserInteractionEnabled = false
super.viewWillAppear(animated)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.viewTextField.isUserInteractionEnabled = true
self.viewTextView.isUserInteractionEnabled = true
}
This will give you the same effect Telegram has.
Related
I present/dismiss ViewController with TextField like a fade. And keyboard do not need to slide on/out of the screen.
For presenting i did that by:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
textField.becomeFirstResponder()
How can i do that for dismiss?
It should not move somehow, i fade out VC and it should fade out with it as well. But insted of that it's moving down
You trying to achive an UI effect which is doesn't comply to Human Interface Guidelines. This usualy means that you you have to find out an trick or a hack to achive it.
Overview:
I would like to set the accessibility focus to the navigation bar's title item.
By default the focus is set from top left, meaning the back button would be on focus.
I would like the title item to be in focus.
Attempts made so far:
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification,
navigationController?.navigationBar.items?.last)
Problem:
The above code makes no difference, the back button is still in focus.
Possible Cause:
Not able to get the item corresponding to the title to be able to set the focus.
Solution 1
I don't like it, but it was the minimum amount of hacking that does not rely on digging through hidden subviews (internal implementation of UINavigationBar view hierarchy).
First in viewWillAppear, I store a backup reference of the back button item,
and then remove the back button item (leftBarButtonItem):
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
backButtonBackup = self.navigationItem.leftBarButtonItem
self.navigationItem.leftBarButtonItem = nil
}
Then I restore the back item, but only after I dispatch the screen changed event in viewDidAppear() :
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, nil)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
self?.navigationItem.leftBarButtonItem = self?.backButtonBackup
}
}
Solution 2:
Disable all accessibility on the nav bar and view controller up until viewDidAppear() is finished:
self.navigationController.navigationBar.accessibilityElementsHidden = true
self.view.accessibilityElementsHidden = true
, and then in viewDidAppear manually dispatching the layout element accessibility focused event to the label subview of UINavigationBar:
UIAccessibilityPostNotification( UIAccessibilityLayoutChangedNotification, self.navigationController.navigationBar.subviews[2].subviews[1])
// The label buried inside the nav bar. Not tested on all iOS versions.
// Alternately you can go digging for the label by checking class types.
// Then use DispatchAsync, to re-enable accessibility on the view and nav bar again...
I'm not a fan of this method either.
DispatchAsync delay in viewDidAppear seems to be needed in any case - and I think both solutions are still horrible.
I invoked UIAccessibilityScreenChangedNotification on navigation title from viewDidLoad and it worked
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification,
self.navigationItem.title);
First, we'll need to create an useful extension:
extension UIViewController {
func setAccessibilityFocus(in view: UIView) {
UIAccessibility.post(notification: .screenChanged, argument: view)
}
}
Then, we'll be able to set our focus in the navigation bar title like this:
setAccessibilityFocus(in: self.navigationController!.navigationBar.subviews[2].subviews[1])
I have two viewcontrollers. The first viewcontroller is collection view controller and i set self.navigationController?.hidesBarsOnSwipe = true in viewDidLoad().
When I push the second viewController from the visible cell of collectionView, the navigation bar is showing in the second viewController but if I scroll the collectionView cell and when push the navigation is not showing.
Can anyone tell me what the problem is?
scrolling is done via swipe gesture, so it triggers your code:
self.navigationController?.hidesBarsOnSwipe = true
because navigation controller is shared between all view controllers presented on top of it, it's properties (like hidden bar) preserves pushing / popping.
Common pattern is to change it's state in overrided lifecycle methods, eg:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.hidesBarsOnSwipe = false
self.navigationController?.setNavigationBarHidden(false, animated: true)
}
and reverting those state in viewWillDisappear
When this property is set to true, an upward swipe hides the navigation bar and toolbar. A downward swipe shows both bars again. If the toolbar does not have any items, it remains visible even after a swipe. The default value of this property is false. (get it from apple)
See the doc https://developer.apple.com/documentation/uikit/uinavigationcontroller/1621883-hidesbarsonswipe
It means when you swipe up it will hide and when swipe down it will shown. That's the reason.
To fix it you can add following code to the other controller
[self.navigationController setNavigationBarHidden:NO animated:YES];
Don't get much insight what exactly you implemented, but try to unhide navigation bar in second view controller.
Add below code in viewDidLoad method of second View controller.
self.navigationController?.isNavigationBarHidden = false
Put this self.navigationController?.hidesBarsOnSwipe = false and this self.navigationController?.setNavigationBarHidden(false, animated: true) in your second view controller.
You might want to move your self.navigationController?.hidesBarsOnSwipe = true from viewDidLoad to viewWillAppear in your first view controller.
If I have a UITabBarController and do this inside one of its view controllers:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
tabBarController?.tabBar.isUserInteractionEnabled = false
}
the first time it shows, I can still change tabs.
If I tap on another tab and tap back on the first tab, then the tabbar is really disabled, but not the first time.
Why? And how do I solve this?
EDIT:
There is one detail I noticed, the tabBarController?.tabBar.isUserInteractionEnabled = false has to be on the second view controller of a navigation controller. In other words:
Say I have that structure
UITabBarController
UINavigationController
UIViewController (1)
UIViewController (2)
UIViewController (3)
So if I add that viewDidAppear code on view controller (2), you can change the tab once, but not the second time (after you navigate to it, obviously).
And there is more, if I go back after navigating to view controller (2), the tab bar becomes "interactable" again, without my setting it to true.
Having a tab bar in view, but not being able to interact with it will probably be confusing and frustrating for the user. And while I don't have the reason or solution for the original question, I have an alternative suggestion:
Hide the tab bar in UIViewController (2):
override func viewDidLoad() {
super.viewDidLoad()
tabBarController?.tabBar.isHidden = true
}
We're putting this in viewDidLoad so it's hidden as soon as the view
appears.
This also requires that you explicitly unhide it in UIViewController (1) for when the user hits the back button. Do it in viewWillAppear since the view was loaded already and we're going back to it.
override func viewWillAppear(_ animated: Bool) {
tabBarController?.tabBar.isHidden = false
}
While I have experienced the same behavior with:
tabBarController?.tabBar.isUserInteractionEnabled = false
..what you CAN do is disable each of the items in the tabBar.items collection, and then re-enable them in the viewWillAppear method of another controller. For example, if you didn't want your users tabbing out of menu #1 once they are inside it, you could do something like this in the subsequent controller(s):
override func viewWillAppear(_ animated: Bool) {
//0th tab items remains enabled as "only choice" for user
self.tabBarController!.tabBar.items![1].isEnabled = false
self.tabBarController!.tabBar.items![2].isEnabled = false
self.tabBarController!.tabBar.items![3].isEnabled = false
self.tabBarController!.tabBar.items![4].isEnabled = false
}
when the user tabs back to first tab (0) (or hits the back button), in that viewController's viewWillAppear method, re-enable the items:
override func viewWillAppear(_ animated: Bool) {
//re-enable tab items
self.tabBarController!.tabBar.items![1].isEnabled = true
self.tabBarController!.tabBar.items![2].isEnabled = true
self.tabBarController!.tabBar.items![3].isEnabled = true
self.tabBarController!.tabBar.items![4].isEnabled = true
}
I am using a Show segue in my application.
Whenever I segue to another screen and press the back bar button, my navigationController.toolbar disappears.
I tried to get rid of it with
navigationController?.toolbar.hidden = false
in my viewDidLoad().
It doesn't work though. Any ideas?
Please add the code in the viewWillAppear() and it should solve the problem you are facing.
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
navigationController?.toolbarHidden = false
}
Remember that viewDidLoad() fires only once during the life cycle of a view controller and in your case , it is in the navigation stack which means it has been already used for that view controller and now when you press back button, it does not work again.
navigationController?.toolbarHidden = false