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])
Related
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.
I am hiding my navigation bar when I scroll by calling: self.navigationController?.setNavigationBarHidden(true, animated: true)
The only problem is that the navbar doesnt get hidden all the way.
Whats even stranger is if I push to a new VC and go back and now try to scroll the navigation bar gets hidden all the way which is what I want.
If it matters I am hiding the navigation bar on VC2 then showing it when I exit back to VC 1.
This is what it looks when I try to hide the navbar first time, it doesnt go up all the way/underlying view showing too much.
If I push the to next VC and go back and now try to hide the navigationbar it works
The my view has a constraint of 0 to top layout so its hugging the top
So how can I make my view always be like in the second image when hiding my navigation bar?
Try this code...
Note: This is a simple approach for your problem. If you want more custom look navBar and status bar look .You should read my previous comment...
Set navigation controller property hidesBarsOnSwipe to true
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
navigationController?.hidesBarsOnSwipe = true
}
I understand your question.
The green part in your second image is not a navigation bar, it is a status bar.
You can hide the status bar as below.
You should implement prefersStatusBarHidden on your view controller(s):
In Swift 2:
override func prefersStatusBarHidden() -> Bool {
return true
}
In Swift 3:
override var prefersStatusBarHidden: Bool {
return true
}
I have created a parallax detail view in swift. Wish to allow nav bar colour and title display when user scrolls down.
Like this example here on the detail view. This is in objective C and I can't figure out the swift version. I'm sure it's simple enough with a few lines of code in the right place.
https://github.com/KMindeguia/movies/blob/master/README.md
I know the nav bar has a .hideswhenuserswips function but can't find anything for this!
Thanks
If you use storyboard for your UINavigationController, you can set like this
Or, you can set in your code like this:
myNavigationController.hidesBarsOnSwipe = true
You can use scrollview delegate methods to show or hide navigation bar.
you can implement scrollViewDidScroll , scrollViewDidEndDecelerating or scrollViewWillBeginDecelerating.
from this delegate methods you can manage your navigation bar.
This component just using simple UIView and implementing UIScrollViewDelegate methods. You can add your custom view in top of parent view and hide it, implement UIScrollViewDelegate methods and track some contentOffset of uiscrollview. Like in this component from lines 237
scrollDelegate methods
Set the NavigationBar in each viewcontroller, if you would not show navigationbar use this code,
self.navigationController?.navigationBarHidden = true
And show the navigationbar in particular viewController put this below code,
self.navigationController?.navigationBarHidden = false
this lines used your method, or you use this code,
override func viewWillAppear(animated: Bool)
{
super.viewWillAppear(animated)
self.navigationController?.navigationBarHidden = true
}
override func viewWillDisappear(animated: Bool)
{
super.viewWillDisappear(animated)
self.navigationController?.navigationBarHidden = false
}
when scrolling to show your navigationbar see this link Hide status bar while scrolling
hope its helpful
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];
I have an App using a Tabbar for basic Navigation. From one of the screens of the Tabbar I want to enter another one that shows a toolbar instead of the Tabbar and a back navigation item on the top.
What is the best way to do this? If I use "Hide Bottom Bar on Push" (aka hidesBottomBarWhenPushed) and add a Toolbar to the screen I can see an animation removing the Tabbar before the Toolbar is placed at the bottom of the screen.
Solution for UITableViewController with toolbar (requires code)
Using code from this answer, I was able to achieve the same effect, but with the toolbar at the bottom of a table view.
Add this to your table view controller:
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self.navigationController setToolbarHidden:NO animated:YES];
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[self.navigationController setToolbarHidden:YES animated:YES];
}
Important note: placing these calls in viewWillAppear and viewWillDisappear instead of viewDidLoad makes this easier to handle, as it will work reliably even for multiple pushes and pops of the same view controller, and you won't have to clean up after it in the previous view controller.
And configure it like this in the storyboard:
Also, enable Hides bottom bar when pushed in the storyboard, or in your code, for the view controller being pushed.
Then you can add toolbar buttons to the toolbar in the storyboard.
Build and run, and you get this effect:
Here's a complete sample project demonstrating this.
Problem Example
Here is my solution,
In the first view controller that has the tabbar do this
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "someSegue" {
if let secondVC = segue.destinationViewController as? InfoTableViewController {
secondVC.hidesBottomBarWhenPushed = true
}
}
}
I also needed this, as my toolbar would re appear in the first VC.
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
navigationController?.toolbarHidden = true
}
To stop the fade up animation of the toolbar, so its just there i used this in the second VC
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.toolbarHidden = false
}
Pure storyboard solution
If you're referring to the issue of the toolbar appearing above the tab bar during the push transition animation, I was able to fix this by adjusting the auto layout constraints on the toolbar in the storyboard (add it manually to your view controller; see my other answer if you're using a UITableViewController or UICollectionViewController and can't do this):
Add a constraint to set the distance to the bottom layout guide to zero:
Double click that constraint to edit it, and set the first item to Bottom (it will be Top by default).
All done! This will result in an effect like this:
Here's my sample project that demonstrates this working as expected. Note that I didn't change any of the code, everything is in the storyboard.
As of Xcode 7, the pure Storyboard solution doesn't work anymore because Xcode wouldn't let you assign the Bottom attribute to the Bottom Layout Guide anymore.
For my project, I used the following setup:
A UITabBarController as initial view controller, going into a
UINavigationController, with root vc set to...
UIRegularViewController, which should behave normally, but spawn a...
UISpecialViewController, which should hide the tab bar and instead display a toolbar. Also, it should hide the status bar, the navigation bar and the tool bar on tap.
Here is what I did to achieve this:
In the storyboard
UITabBarController: set Tab Bar Translucency to NO
UISpecialViewController: Set Simulated Metrics like so
Status Bar: None
Top Bar: Opaque Nav Bar
Bottom Bar: Opaque Toolbar
Set Extended Edges like this:
Under Top Bars: NO
Under Bottom Bars: YES
Under Opaque Bars: YES
Do not drag a UIToolBar into UISpecialViewController !
In the Implementations
// in UISpecialViewController.m
- (void)viewWillAppear:(BOOL)animated {
self.navigationController.toolbarHidden = NO;
self.navigationController.hidesBarsOnTap = YES;
}
- (void)viewWillDisappear:(BOOL)animated {
self.navigationController.toolbarHidden = YES;
self.navigationController.hidesBarsOnTap = NO;
}
- (BOOL)prefersStatusBarHidden {
return self.navigationController.navigationBarHidden;
}
Here is the Demo Code.
This is the result:
In fact, UIKit has already configured how Toolbar and Tabbar change in the page switching animation.
I also have this situation with you today, and the final solution surprised me.
For example, page A to page B, page A displays Tabbar, not Toolbar, page B does not display Toolbar, and does not display Tabbar.
At this time, B needs to set hidesBottomBarWhenPushed to true, which is necessary.
Then, in the declaration cycle of the two ViewControllers, in the viewWillDisappear of A and the viewWillAppear of B, if you set the navigation controller setToolbarHidden, this animation problem will occur.
If you set it in viewDidDisappear of A and viewDidAppear of B, the problem is solved. Although the toolbar will have a delayed animation, it is always better than the wrong animation.
Finally add:
The order of A and B life cycle function calls is:
A - viewWillDisappear
B - viewWillAppear
A - viewDidDisappear
B - viewDidAppear
These four methods are interleaved.
Using Xcode 12.4 iOS 14.4
for those who struggle with this issue and try solutions above with no luck.
let's say A is with tabBar only, B is only showing toolbar
remember to set hidesBottomBarWhenPushed = true in B's init
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
hidesBottomBarWhenPushed = true
}
implement these below in B. (no need to do anything in A)
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.setToolbarHidden(true, animated: false)
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
navigationController?.setToolbarHidden(false, animated: false)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
navigationController?.setToolbarHidden(true, animated: true)
}
that's it!!
p.s. if you want to remove the toolbar animation from bottom up then add this
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
navigationController?.toolbar.layer.removeAnimation(forKey: "position")
}